<?xml version="1.0" encoding="UTF-8"?>
<TaskerData sr="" dvi="1" tv="6.7.6-beta">
<Task sr="task915"><cdate>1788360121429</cdate><edate>1788360121429</edate><id>915</id>
<nme>ZOE ABRP + RED LIVE v15</nme><pri>100</pri>
<Action sr="act0" ve="7"><code>474</code>
<Str sr="arg0" ve="3">/*
 ZOE ZE50 LIVE -&gt; ABRP + RED v15
 Persistent Bluetooth SPP, automatic reconnect.
 No Tasker plugin.

 Main changes vs v11:
 - CAN fast sampling continues independently of ABRP cadence.
 - ABRP telemetry cadence is adaptive:
      charging     = 30 s
      not charging = 3 s
 - HTTP errors do not stop the CAN loop.
 - Power remains updated every fast cycle.
 - No GPS/lat/lon sent.
 - Stop by setting %ZOE_LIVE_STOP = 1.

 MAC: 10:21:3E:4F:6B:47
*/

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

/* configuration */
String cr = String.valueOf((char)13);
String MAC = "10:21:3E:4F:6B:47";
String SPP = "00001101-0000-1000-8000-00805F9B34FB";

String ABRP_API_KEY = "78f9e662-cbcc-4064-8fa9-1c8198564dd3";
String ABRP_TOKEN   = "15e7573f-fa2b-49f8-a5dc-65014de88341";

long SLOW_PERIOD_MS = 60000L;
long ABRP_DRIVE_PERIOD_MS = 3000L;
long ABRP_CHARGE_PERIOD_MS = 30000L;
long RECONNECT_WAIT_MS = 30000L;

int FAST_FAIL_LIMIT = 3;

/* patterns */
Pattern pSoc  = Pattern.compile("(?i)629002([0-9A-F]{4})");
Pattern pSoh  = Pattern.compile("(?i)629003([0-9A-F]{4})");
Pattern pVolt = Pattern.compile("(?i)629005([0-9A-F]{4})");
Pattern pCurr = Pattern.compile("(?i)62900D([0-9A-F]{8})");
Pattern pTemp = Pattern.compile("(?i)629012([0-9A-F]{4})");
Pattern pChg  = Pattern.compile("(?i)629018([0-9A-F]{4})");
Pattern pEnd  = Pattern.compile("(?i)629019([0-9A-F]{2})");

BluetoothSocket socket = null;
InputStream in = null;
OutputStream out = null;
Thread reader = null;
StringBuffer rx = null;
boolean[] stopReader = null;

long lastSlow = 0L;
long lastAbrp = 0L;
int fastFailures = 0;

/* Always start a fresh live session. */
tasker.setVariable("ZOE_LIVE_STOP","0");
tasker.setVariable("ZOE_LIVE_RECONNECT_COUNT","0");
String stopVar = tasker.getVariable("ZOE_LIVE_STOP");

tasker.setVariable("ZOE_LIVE_ERROR","");
tasker.setVariable("ZOE_LIVE_CONNECTED","0");
tasker.setVariable("ZOE_LIVE_STATE","Starting");

/* ------------ outer reconnect loop ------------ */

while (true) {

    stopVar = tasker.getVariable("ZOE_LIVE_STOP");
    if (stopVar != null &amp;&amp; stopVar.equals("1")) break;

    boolean connected = false;

    /* ------------ connect / initialize ------------ */

    try {

        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
        if (adapter == null) throw new Exception("Bluetooth indisponible");

        BluetoothDevice device = adapter.getRemoteDevice(MAC);
        adapter.cancelDiscovery();

        tasker.setVariable("ZOE_LIVE_STATE","Connecting");

        socket = device.createRfcommSocketToServiceRecord(
            UUID.fromString(SPP)
        );
        socket.connect();

        in = socket.getInputStream();
        out = socket.getOutputStream();

        final InputStream rin = in;
        rx = new StringBuffer();
        stopReader = new boolean[] { false };
        final StringBuffer readerRx = rx;
        final boolean[] readerStop = stopReader;

        reader = new Thread(new Runnable() {
            public void run() {
                byte[] buf = new byte[512];
                try {
                    while (!readerStop[0]) {
                        int n = rin.read(buf);
                        if (n &lt; 0) break;
                        if (n &gt; 0) {
                            synchronized (readerRx) {
                                readerRx.append(
                                    new String(buf,0,n,"US-ASCII")
                                );
                                if (readerRx.length() &gt; 40000) {
                                    readerRx.delete(0,15000);
                                }
                            }
                        }
                    }
                } catch (Exception e) {
                }
            }
        });
        reader.start();

        tasker.setVariable("ZOE_LIVE_STATE","Initializing");

        out.write(("ATZ"+cr).getBytes("US-ASCII")); out.flush();
        Thread.sleep(2200);
        out.write(("ATE0"+cr).getBytes("US-ASCII")); out.flush();
        Thread.sleep(400);
        out.write(("ATL0"+cr).getBytes("US-ASCII")); out.flush();
        Thread.sleep(300);
        out.write(("ATS0"+cr).getBytes("US-ASCII")); out.flush();
        Thread.sleep(300);
        out.write(("ATH0"+cr).getBytes("US-ASCII")); out.flush();
        Thread.sleep(300);
        out.write(("ATSP7"+cr).getBytes("US-ASCII")); out.flush();
        Thread.sleep(600);
        out.write(("ATSHDADBF1"+cr).getBytes("US-ASCII")); out.flush();
        Thread.sleep(300);
        out.write(("ATFCSH18DADBF1"+cr).getBytes("US-ASCII")); out.flush();
        Thread.sleep(300);
        out.write(("ATCRA18DAF1DB"+cr).getBytes("US-ASCII")); out.flush();
        Thread.sleep(500);

        connected = true;
        fastFailures = 0;
        lastSlow = 0L;
        lastAbrp = 0L;

        tasker.setVariable("ZOE_LIVE_CONNECTED","1");
        tasker.setVariable("ZOE_LIVE_STATE","Connected");
        tasker.setVariable("ZOE_LIVE_FAST_FAILURES","0");

    } catch (Exception e) {

        tasker.setVariable("ZOE_LIVE_CONNECTED","0");
        tasker.setVariable("ZOE_LIVE_STATE","Reconnect wait");

        String fullError =
            "Connect: " + e.getClass().getName() + ": " +
            String.valueOf(e.getMessage());
        tasker.setVariable("ZOE_LIVE_ERROR",fullError);

        String countText=tasker.getVariable("ZOE_LIVE_RECONNECT_COUNT");
        int reconnectCount=0;
        try{
            if(countText!=null &amp;&amp; countText.length()&gt;0)
                reconnectCount=Integer.parseInt(countText);
        }catch(Exception ignored){}
        reconnectCount++;
        tasker.setVariable("ZOE_LIVE_RECONNECT_COUNT",String.valueOf(reconnectCount));

        try { if (socket != null) socket.close(); } catch (Exception x) {}
        socket=null; in=null; out=null;

        stopVar = tasker.getVariable("ZOE_LIVE_STOP");
        if (stopVar != null &amp;&amp; stopVar.equals("1")) break;

        tasker.setVariable("ZOE_LIVE_STATE","Bluetooth reset wait");
        Thread.sleep(5000L);

        stopVar = tasker.getVariable("ZOE_LIVE_STOP");
        if (stopVar != null &amp;&amp; stopVar.equals("1")) break;

        tasker.setVariable("ZOE_LIVE_STATE","Waiting reconnect 30s");
        Thread.sleep(RECONNECT_WAIT_MS);
        continue;
    }

    /* ------------ connected loop ------------ */

    try {

        while (connected) {

            stopVar = tasker.getVariable("ZOE_LIVE_STOP");
            if (stopVar != null &amp;&amp; stopVar.equals("1")) {
                connected=false;
                break;
            }

            /* ===== FAST voltage ===== */

            int start = 0;
            synchronized (rx) { start=rx.length(); }

            boolean gotVoltage=false;

            try {
                out.write(("229005"+cr).getBytes("US-ASCII"));
                out.flush();

                long deadline=System.currentTimeMillis()+1800L;
                String part="";

                while(System.currentTimeMillis()&lt;deadline){
                    synchronized(rx){
                        if(rx.length()&gt;start) part=rx.substring(start);
                    }

                    Matcher mv=pVolt.matcher(part);
                    if(mv.find()){
                        int rawV=Integer.parseInt(mv.group(1),16);
                        double volts=rawV*0.1;
                        tasker.setVariable(
                            "ZOE_VOLTAGE",
                            String.format(java.util.Locale.US,"%.1f",volts)
                        );
                        gotVoltage=true;
                        break;
                    }
                    Thread.sleep(40);
                }
            } catch(Exception e){ gotVoltage=false; }

            /* ===== FAST current ===== */

            start=0;
            synchronized(rx){start=rx.length();}

            boolean gotCurrent=false;

            try {
                out.write(("22900D"+cr).getBytes("US-ASCII"));
                out.flush();

                long deadline=System.currentTimeMillis()+1800L;
                String part="";

                while(System.currentTimeMillis()&lt;deadline){
                    synchronized(rx){
                        if(rx.length()&gt;start) part=rx.substring(start);
                    }

                    Matcher mc=pCurr.matcher(part);
                    if(mc.find()){
                        long rawI=Long.parseLong(mc.group(1),16);
                        double amps=(rawI-48000.0)*0.025;

                        tasker.setVariable(
                            "ZOE_CURRENT",
                            String.format(java.util.Locale.US,"%.2f",amps)
                        );

                        gotCurrent=true;
                        break;
                    }
                    Thread.sleep(40);
                }
            } catch(Exception e){ gotCurrent=false; }

            /* ===== power / charge state ===== */

            if(gotVoltage &amp;&amp; gotCurrent){

                fastFailures=0;
                tasker.setVariable("ZOE_LIVE_FAST_FAILURES","0");

                String vs=tasker.getVariable("ZOE_VOLTAGE");
                String is=tasker.getVariable("ZOE_CURRENT");

                if(vs!=null &amp;&amp; is!=null &amp;&amp;
                   vs.length()&gt;0 &amp;&amp; is.length()&gt;0){

                    double volts=Double.parseDouble(vs);
                    double amps=Double.parseDouble(is);
                    double rawPower=volts*amps/1000.0;

                    String endFlag=tasker.getVariable("ZOE_CHG_END");
                    boolean charging=false;

                    if(endFlag!=null &amp;&amp;
                       endFlag.equals("0") &amp;&amp;
                       amps&gt;0.2){
                        charging=true;
                    }

                    double abrpPower=rawPower;
                    if(charging) abrpPower=-Math.abs(rawPower);

                    tasker.setVariable(
                        "ZOE_POWER_ABRP",
                        String.format(java.util.Locale.US,"%.2f",abrpPower)
                    );

                    tasker.setVariable(
                        "ZOE_IS_CHARGING",
                        charging ? "1" : "0"
                    );

                    tasker.setVariable(
                        "ZOE_LIVE_LAST_FAST",
                        String.valueOf(System.currentTimeMillis()/1000L)
                    );
                }

            } else {

                fastFailures++;
                tasker.setVariable(
                    "ZOE_LIVE_FAST_FAILURES",
                    String.valueOf(fastFailures)
                );
            }

            if(fastFailures &gt;= FAST_FAIL_LIMIT){
                tasker.setVariable(
                    "ZOE_LIVE_ERROR",
                    "CAN/ECU lost - reconnecting"
                );
                tasker.setVariable(
                    "ZOE_LIVE_STATE","ECU lost"
                );
                connected=false;
                break;
            }

            /* ===== SLOW CAN data every 60 sec ===== */

            long now=System.currentTimeMillis();

            if(lastSlow==0L || now-lastSlow&gt;=SLOW_PERIOD_MS){

                /* helper pattern repeated explicitly for BeanShell compatibility */

                start=0;
                synchronized(rx){start=rx.length();}
                out.write(("229002"+cr).getBytes("US-ASCII")); out.flush();
                long deadline=System.currentTimeMillis()+2200L;
                String part="";
                while(System.currentTimeMillis()&lt;deadline){
                    synchronized(rx){
                        if(rx.length()&gt;start) part=rx.substring(start);
                    }
                    if(pSoc.matcher(part).find()) break;
                    Thread.sleep(40);
                }
                Matcher ms=pSoc.matcher(part);
                if(ms.find()){
                    int raw=Integer.parseInt(ms.group(1),16);
                    tasker.setVariable("ZOE_SOC",
                        String.format(java.util.Locale.US,"%.2f",raw*0.01));
                }

                start=0;
                synchronized(rx){start=rx.length();}
                out.write(("229003"+cr).getBytes("US-ASCII")); out.flush();
                deadline=System.currentTimeMillis()+2200L;
                part="";
                while(System.currentTimeMillis()&lt;deadline){
                    synchronized(rx){
                        if(rx.length()&gt;start) part=rx.substring(start);
                    }
                    if(pSoh.matcher(part).find()) break;
                    Thread.sleep(40);
                }
                Matcher msoh=pSoh.matcher(part);
                if(msoh.find()){
                    int raw=Integer.parseInt(msoh.group(1),16);
                    tasker.setVariable("ZOE_SOH",
                        String.format(java.util.Locale.US,"%.2f",raw*0.01));
                }

                start=0;
                synchronized(rx){start=rx.length();}
                out.write(("229012"+cr).getBytes("US-ASCII")); out.flush();
                deadline=System.currentTimeMillis()+2200L;
                part="";
                while(System.currentTimeMillis()&lt;deadline){
                    synchronized(rx){
                        if(rx.length()&gt;start) part=rx.substring(start);
                    }
                    if(pTemp.matcher(part).find()) break;
                    Thread.sleep(40);
                }
                Matcher mt=pTemp.matcher(part);
                if(mt.find()){
                    int raw=Integer.parseInt(mt.group(1),16);
                    double temp=(raw-640.0)*0.0625;
                    tasker.setVariable("ZOE_BATT_TEMP",
                        String.format(java.util.Locale.US,"%.2f",temp));
                }

                start=0;
                synchronized(rx){start=rx.length();}
                out.write(("229018"+cr).getBytes("US-ASCII")); out.flush();
                deadline=System.currentTimeMillis()+2200L;
                part="";
                while(System.currentTimeMillis()&lt;deadline){
                    synchronized(rx){
                        if(rx.length()&gt;start) part=rx.substring(start);
                    }
                    if(pChg.matcher(part).find()) break;
                    Thread.sleep(40);
                }
                Matcher mch=pChg.matcher(part);
                if(mch.find()){
                    int raw=Integer.parseInt(mch.group(1),16);
                    tasker.setVariable("ZOE_CHG_MAX",
                        String.format(java.util.Locale.US,"%.2f",raw*0.01));
                }

                start=0;
                synchronized(rx){start=rx.length();}
                out.write(("229019"+cr).getBytes("US-ASCII")); out.flush();
                deadline=System.currentTimeMillis()+2200L;
                part="";
                while(System.currentTimeMillis()&lt;deadline){
                    synchronized(rx){
                        if(rx.length()&gt;start) part=rx.substring(start);
                    }
                    if(pEnd.matcher(part).find()) break;
                    Thread.sleep(40);
                }
                Matcher mend=pEnd.matcher(part);
                if(mend.find()){
                    int raw=Integer.parseInt(mend.group(1),16);
                    tasker.setVariable(
                        "ZOE_CHG_END",
                        String.valueOf(raw&amp;1)
                    );
                }

                lastSlow=System.currentTimeMillis();
            }

            /* ===== adaptive ABRP telemetry ===== */

            now=System.currentTimeMillis();

            String chargingNow=tasker.getVariable("ZOE_IS_CHARGING");
            boolean isChargingNow =
                chargingNow!=null &amp;&amp; chargingNow.equals("1");

            long abrpPeriod =
                isChargingNow
                ? ABRP_CHARGE_PERIOD_MS
                : ABRP_DRIVE_PERIOD_MS;

            /*
             * Immediate cadence adaptation:
             * if state changes from charging to driving, next POST can
             * happen at the next loop instead of waiting 30 s.
             */
            if(lastAbrp==0L || now-lastAbrp&gt;=abrpPeriod){

                String soc=tasker.getVariable("ZOE_SOC");
                String soh=tasker.getVariable("ZOE_SOH");
                String power=tasker.getVariable("ZOE_POWER_ABRP");
                String temp=tasker.getVariable("ZOE_BATT_TEMP");
                String voltage=tasker.getVariable("ZOE_VOLTAGE");
                String current=tasker.getVariable("ZOE_CURRENT");

                if(soc!=null &amp;&amp; soh!=null &amp;&amp; power!=null &amp;&amp;
                   temp!=null &amp;&amp; voltage!=null &amp;&amp; current!=null){

                    if(chargingNow==null || chargingNow.length()==0)
                        chargingNow="0";

                    long utc=System.currentTimeMillis()/1000L;

                    StringBuilder tlm=new StringBuilder();
                    tlm.append("{");
                    tlm.append("\"utc\":").append(utc);
                    tlm.append(",\"soc\":").append(soc);
                    tlm.append(",\"soh\":").append(soh);
                    tlm.append(",\"power\":").append(power);
                    tlm.append(",\"batt_temp\":").append(temp);
                    tlm.append(",\"voltage\":").append(voltage);
                    tlm.append(",\"current\":").append(current);
                    tlm.append(",\"is_charging\":").append(chargingNow);
                    tlm.append(",\"is_dcfc\":0");
                    tlm.append(",\"is_parked\":").append(
                        chargingNow.equals("1") ? "1" : "0"
                    );
                    tlm.append("}");

                    String body=
                        "{\"tlm\":"+tlm.toString()+
                        ",\"token\":\""+ABRP_TOKEN+"\"}";

                    HttpURLConnection conn=null;

                    try{
                        URL url=new URL(
                            "https://api.iternio.com/1/tlm/send"
                        );

                        conn=(HttpURLConnection)url.openConnection();
                        conn.setRequestMethod("POST");
                        conn.setConnectTimeout(8000);
                        conn.setReadTimeout(8000);
                        conn.setDoOutput(true);
                        conn.setRequestProperty(
                            "Content-Type",
                            "application/json; charset=utf-8"
                        );
                        conn.setRequestProperty(
                            "Authorization",
                            "APIKEY "+ABRP_API_KEY
                        );

                        OutputStream aos=conn.getOutputStream();
                        aos.write(body.getBytes("UTF-8"));
                        aos.flush();
                        aos.close();

                        int code=conn.getResponseCode();

                        tasker.setVariable(
                            "ZOE_ABRP_HTTP_CODE",
                            String.valueOf(code)
                        );

                        tasker.setVariable(
                            "ZOE_ABRP_LAST_SEND",
                            String.valueOf(
                                System.currentTimeMillis()/1000L
                            )
                        );

                        tasker.setVariable(
                            "ZOE_ABRP_PERIOD",
                            String.valueOf(abrpPeriod/1000L)
                        );

                        tasker.setVariable(
                            "ZOE_ABRP_POWER_SENT",
                            power
                        );

                        InputStream ris =
                            (code&gt;=200 &amp;&amp; code&lt;400)
                            ? conn.getInputStream()
                            : conn.getErrorStream();

                        String response="";

                        if(ris!=null){
                            BufferedReader br=
                                new BufferedReader(
                                    new InputStreamReader(ris,"UTF-8")
                                );
                            String line;
                            while((line=br.readLine())!=null)
                                response=response+line;
                            br.close();
                        }

                        tasker.setVariable(
                            "ZOE_ABRP_RESPONSE",
                            response
                        );

                        if(code&gt;=200 &amp;&amp; code&lt;300)
                            tasker.setVariable(
                                "ZOE_ABRP_ERROR",""
                            );
                        else
                            tasker.setVariable(
                                "ZOE_ABRP_ERROR",
                                "HTTP "+code+": "+response
                            );

                    } catch(Exception httpError){

                        tasker.setVariable(
                            "ZOE_ABRP_ERROR",
                            "HTTP exception: "+httpError.toString()
                        );

                    } finally{

                        if(conn!=null) conn.disconnect();
                    }

                    /*
                     * RED endpoint: exact JSON object validated with curl.
                     * Completely isolated: failure here never stops CAN/ABRP.
                     */
                    HttpURLConnection redConn=null;
                    try{
                        URL redUrl=new URL("https://red.engles.fr/openapi/zoe");
                        redConn=(HttpURLConnection)redUrl.openConnection();
                        redConn.setRequestMethod("POST");
                        redConn.setConnectTimeout(8000);
                        redConn.setReadTimeout(8000);
                        redConn.setDoOutput(true);
                        redConn.setRequestProperty(
                            "Content-Type","application/json; charset=utf-8"
                        );
                        redConn.setRequestProperty("Accept","application/json");

                        String redJson=tlm.toString();
                        tasker.setVariable("ZOE_RED_SENT_JSON",redJson);

                        OutputStream redOut=redConn.getOutputStream();
                        redOut.write(redJson.getBytes("UTF-8"));
                        redOut.flush();
                        redOut.close();

                        int redCode=redConn.getResponseCode();
                        tasker.setVariable(
                            "ZOE_RED_HTTP_CODE",String.valueOf(redCode)
                        );

                        InputStream redIn=
                            (redCode&gt;=200 &amp;&amp; redCode&lt;400)
                            ? redConn.getInputStream()
                            : redConn.getErrorStream();

                        String redResponse="";
                        if(redIn!=null){
                            BufferedReader redBr=new BufferedReader(
                                new InputStreamReader(redIn,"UTF-8")
                            );
                            String redLine;
                            while((redLine=redBr.readLine())!=null)
                                redResponse=redResponse+redLine;
                            redBr.close();
                        }

                        tasker.setVariable("ZOE_RED_RESPONSE",redResponse);
                        tasker.setVariable(
                            "ZOE_RED_LAST_SEND",
                            String.valueOf(System.currentTimeMillis()/1000L)
                        );

                        if(redCode&gt;=200 &amp;&amp; redCode&lt;300)
                            tasker.setVariable("ZOE_RED_ERROR","");
                        else
                            tasker.setVariable(
                                "ZOE_RED_ERROR",
                                "HTTP "+redCode+": "+redResponse
                            );

                    }catch(Exception redError){
                        tasker.setVariable(
                            "ZOE_RED_ERROR",
                            "HTTP exception: "+redError.toString()
                        );
                    }finally{
                        if(redConn!=null) redConn.disconnect();
                    }

                    lastAbrp=System.currentTimeMillis();
                }
            }

            tasker.setVariable(
                "ZOE_LIVE_STATE",
                isChargingNow ? "Charging" : "Live"
            );

            /*
             * Short sleep. Actual CAN sample interval is dominated by
             * the two request/response transactions.
             */
            Thread.sleep(500);
        }

    } catch(Exception sessionError){

        tasker.setVariable(
            "ZOE_LIVE_ERROR",
            "Session: "+sessionError.getClass().getName()+": "+
            String.valueOf(sessionError.getMessage())
        );
        tasker.setVariable(
            "ZOE_LIVE_STATE",
            "Session lost"
        );

    } finally{

        tasker.setVariable("ZOE_LIVE_CONNECTED","0");

        try{
            if(stopReader!=null) stopReader[0]=true;
        }catch(Exception e){}

        try{
            if(socket!=null) socket.close();
        }catch(Exception e){}

        socket=null;
        in=null;
        out=null;
        reader=null;
    }

    stopVar=tasker.getVariable("ZOE_LIVE_STOP");
    if(stopVar!=null &amp;&amp; stopVar.equals("1")) break;

    tasker.setVariable(
        "ZOE_LIVE_STATE",
        "Waiting reconnect"
    );

    Thread.sleep(RECONNECT_WAIT_MS);
}

/* exit */
tasker.setVariable("ZOE_LIVE_CONNECTED","0");
tasker.setVariable("ZOE_LIVE_STATE","Stopped");
</Str>
<Str sr="arg1" ve="3"></Str><Str sr="arg2" ve="3"></Str><Int sr="arg3" val="0"/>
</Action></Task></TaskerData>