<?xml version="1.0" encoding="UTF-8"?>
<TaskerData sr="" dvi="1" tv="6.7.6-beta">
    <Task sr="task930">
        <cdate>1788370000000</cdate>
        <edate>1788370000000</edate>
        <id>930</id>
        <nme>ZOE READ v18</nme>
        <pri>100</pri>
        <Action sr="act0" ve="7">
            <code>474</code>
            <Str sr="arg0" ve="3"><![CDATA[
/*
 * ZOE READ v18
 * Renault ZOE ZE50 Phase 2 / 2021
 *
 * v18:
 * - 22900D signed battery current
 * - power calculated directly from V*I
 * - 222003 EVC speed
 * - 222B85 EVC plug state
 * - isCharging = plugged + stopped + charging current
 * - driving always forces isCharging=0
 * - no use of 229019 for isCharging
 * - telemetry: 3s driving/not charging, 30s charging
 *
 * Existing Java global:
 *     zOE_SOCKET
 */

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

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

/* LBC */
Pattern pV=Pattern.compile("(?i)629005([0-9A-F]{4})");
Pattern pI=Pattern.compile("(?i)62900D([0-9A-F]{8})");
Pattern pS=Pattern.compile("(?i)629002([0-9A-F]{4})");
Pattern pH=Pattern.compile("(?i)629003([0-9A-F]{4})");
Pattern pT=Pattern.compile("(?i)629012([0-9A-F]{4})");
Pattern pC=Pattern.compile("(?i)629018([0-9A-F]{4})");

/* EVC */
Pattern pSpeed=Pattern.compile("(?i)622003([0-9A-F]{4})");
Pattern pPlug=Pattern.compile("(?i)622B85([0-9A-F]{2})");

tasker.setVariable("ZOE_LIVE_ERROR","");

/* ---------- reconnect ---------- */
void reconnectZoe() throws Exception {
    Object old=tasker.getJavaVariable("zOE_SOCKET");
    try{ if(old!=null)((BluetoothSocket)old).close(); }catch(Exception ignored){}
    tasker.setJavaVariable("zOE_SOCKET",null);

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

    BluetoothDevice d=a.getRemoteDevice(MAC);
    a.cancelDiscovery();

    BluetoothSocket s=d.createRfcommSocketToServiceRecord(UUID.fromString(SPP));
    s.connect();

    InputStream i=s.getInputStream();
    OutputStream o=s.getOutputStream();
    tasker.setJavaVariable("zOE_SOCKET",s);

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

    o.write(("ATSHDADBF1"+cr).getBytes("US-ASCII")); o.flush(); Thread.sleep(300);
    o.write(("ATFCSH18DADBF1"+cr).getBytes("US-ASCII")); o.flush(); Thread.sleep(300);
    o.write(("ATCRA18DAF1DB"+cr).getBytes("US-ASCII")); o.flush(); Thread.sleep(500);

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

/* ---------- read one ELM response ---------- */
String readElmResponse(InputStream in,String command) throws Exception {
    OutputStream out=((BluetoothSocket)tasker.getJavaVariable("zOE_SOCKET")).getOutputStream();

    out.write((command+cr).getBytes("US-ASCII"));
    out.flush();

    StringBuffer r=new StringBuffer();
    long end=System.currentTimeMillis()+3000L;

    while(System.currentTimeMillis()<end){
        try{
            byte[] b=new byte[256];
            int n=in.read(b);
            if(n<0)throw new Exception("Bluetooth EOF");
            if(n>0){
                r.append(new String(b,0,n,"US-ASCII"));
                if(r.indexOf(">")>=0)break;
            }
        }catch(java.net.SocketTimeoutException e){}
    }
    return r.toString();
}

/* ---------- one/two attempt loop ---------- */
boolean success=false;
Exception lastError=null;

for(int attempt=0;attempt<2 && !success;attempt++){
    try{
        Object so=tasker.getJavaVariable("zOE_SOCKET");
        if(so==null || so==void){
            reconnectZoe();
            so=tasker.getJavaVariable("zOE_SOCKET");
        }

        BluetoothSocket socket=(BluetoothSocket)so;
        if(!socket.isConnected()){
            reconnectZoe();
            socket=(BluetoothSocket)tasker.getJavaVariable("zOE_SOCKET");
        }

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

        /* Clear pending bytes */
        try{
            int av=in.available();
            while(av>0){
                byte[] dump=new byte[Math.min(av,512)];
                int dn=in.read(dump);
                if(dn<=0)break;
                av=in.available();
            }
        }catch(Exception ignored){}

        /* =====================================================
         * LBC FAST
         * ===================================================== */
        out.write(("ATSHDADBF1"+cr).getBytes("US-ASCII")); out.flush(); Thread.sleep(80);
        out.write(("ATFCSH18DADBF1"+cr).getBytes("US-ASCII")); out.flush(); Thread.sleep(80);
        out.write(("ATCRA18DAF1DB"+cr).getBytes("US-ASCII")); out.flush(); Thread.sleep(80);

        /* Voltage */
        String vText=readElmResponse(in,"229005");
        Matcher mv=pV.matcher(vText);
        if(!mv.find())throw new Exception("229005 decode RX="+vText);

        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));

        /* Current: signed */
        String iText=readElmResponse(in,"22900D");
        Matcher mi=pI.matcher(iText);
        if(!mi.find())throw new Exception("22900D decode RX="+iText);

        long rawI=Long.parseLong(mi.group(1),16);
        double amps=(rawI-48000.0)*0.025;

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

        /*
         * IMPORTANT:
         * Keep physical sign of current.
         */
        double rawPower=volts*amps/1000.0;

        /*
         * ABRP convention used by this task:
         * battery input/charging = negative
         * battery output/driving = positive
         *
         * Therefore invert the physical battery power.
         */
        double abrpPower=-rawPower;

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

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

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

        /* =====================================================
         * EVC
         * ===================================================== */
        out.write(("ATSHDADAF1"+cr).getBytes("US-ASCII")); out.flush(); Thread.sleep(80);
        out.write(("ATFCSH18DADAF1"+cr).getBytes("US-ASCII")); out.flush(); Thread.sleep(80);
        out.write(("ATCRA18DAF1DA"+cr).getBytes("US-ASCII")); out.flush(); Thread.sleep(80);

        /* Speed */
        String speedText=readElmResponse(in,"222003");
        Matcher mSpeed=pSpeed.matcher(speedText);

        double speedKmh=-1.0;

        if(mSpeed.find()){
            int rawSpeed=Integer.parseInt(mSpeed.group(1),16);
            speedKmh=rawSpeed*0.01;
            tasker.setVariable("ZOE_SPEED",
                String.format(java.util.Locale.US,"%.2f",speedKmh));
        }else{
            String oldSpeed=tasker.getVariable("ZOE_SPEED");
            if(oldSpeed!=null && oldSpeed.length()>0){
                try{speedKmh=Double.parseDouble(oldSpeed);}catch(Exception ignored){}
            }
        }

        /* Plug */
        String plugText=readElmResponse(in,"222B85");
        Matcher mPlug=pPlug.matcher(plugText);

        boolean plugged=false;

        if(mPlug.find()){
            int plugRaw=Integer.parseInt(mPlug.group(1),16);

            /*
             * ZE50 EVC:
             * B4 = plugged
             */
            plugged=(plugRaw==0xB4);

            tasker.setVariable("ZOE_PLUGGED",plugged?"1":"0");
            tasker.setVariable("ZOE_PLUG_RAW",
                String.format(java.util.Locale.US,"%02X",plugRaw));
        }else{
            String oldPlug=tasker.getVariable("ZOE_PLUGGED");
            if(oldPlug!=null && oldPlug.length()>0)
                plugged=oldPlug.equals("1");
        }

        /* =====================================================
         * isCharging
         * ===================================================== */
        boolean charging=false;

        /*
         * If moving: NEVER charging.
         * This protects against regenerative braking.
         */
        if(speedKmh>=0.0 && speedKmh>1.0){
            charging=false;
        }else if(speedKmh>=0.0){
            /*
             * Stopped:
             * cable must be plugged and battery current must
             * indicate battery input.
             *
             * NOTE: sign convention of 22900D should be
             * confirmed with one real charge sample.
             */
            charging=plugged && amps>2.0;
        }else{
            /*
             * Speed unavailable:
             * conservative choice = do not declare charging
             * from current alone.
             */
            charging=false;
        }

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

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

        tasker.setVariable("ZOE_LIVE_PLUGGED",plugged?"1":"0");

        /* =====================================================
         * SLOW LBC - max once/minute
         * ===================================================== */
        String lastSlow=tasker.getVariable("ZOE_LAST_SLOW_MS");
        long now=System.currentTimeMillis();

        if(lastSlow==null || lastSlow.length()==0 ||
           now-Long.parseLong(lastSlow)>=60000L){

            out.write(("ATSHDADBF1"+cr).getBytes("US-ASCII")); out.flush(); Thread.sleep(80);
            out.write(("ATFCSH18DADBF1"+cr).getBytes("US-ASCII")); out.flush(); Thread.sleep(80);
            out.write(("ATCRA18DAF1DB"+cr).getBytes("US-ASCII")); out.flush(); Thread.sleep(80);

            /* SOC */
            String rs=readElmResponse(in,"229002");
            Matcher ms=pS.matcher(rs);
            if(!ms.find())throw new Exception("229002 decode RX="+rs);
            int sraw=Integer.parseInt(ms.group(1),16);
            tasker.setVariable("ZOE_SOC",
                String.format(java.util.Locale.US,"%.2f",sraw*0.01));

            /* SOH */
            rs=readElmResponse(in,"229003");
            Matcher mh=pH.matcher(rs);
            if(!mh.find())throw new Exception("229003 decode RX="+rs);
            int hraw=Integer.parseInt(mh.group(1),16);
            tasker.setVariable("ZOE_SOH",
                String.format(java.util.Locale.US,"%.2f",hraw*0.01));

            /* Temperature */
            rs=readElmResponse(in,"229012");
            Matcher mt=pT.matcher(rs);
            if(!mt.find())throw new Exception("229012 decode RX="+rs);
            int traw=Integer.parseInt(mt.group(1),16);
            tasker.setVariable("ZOE_BATT_TEMP",
                String.format(java.util.Locale.US,"%.2f",
                    (traw-640.0)*0.0625));

            /* Max charge power */
            rs=readElmResponse(in,"229018");
            Matcher mc=pC.matcher(rs);
            if(!mc.find())throw new Exception("229018 decode RX="+rs);
            int craw=Integer.parseInt(mc.group(1),16);
            tasker.setVariable("ZOE_CHG_MAX",
                String.format(java.util.Locale.US,"%.2f",craw*0.01));

            /*
             * 229019 intentionally NOT used for isCharging.
             */

            tasker.setVariable("ZOE_LAST_SLOW_MS",
                String.valueOf(System.currentTimeMillis()));
        }

        /* =====================================================
         * TELEMETRY
         * ===================================================== */
        String ch2=tasker.getVariable("ZOE_IS_CHARGING");
        if(ch2==null)ch2="0";
        boolean chargeNow=ch2.equals("1");

        long period=chargeNow?30000L:3000L;

        String lastSend=tasker.getVariable("ZOE_ABRP_LAST_SEND_MS");
        long ts=System.currentTimeMillis();

        if(lastSend==null || lastSend.length()==0 ||
           ts-Long.parseLong(lastSend)>=period){

            String soc=tasker.getVariable("ZOE_SOC");
            String soh=tasker.getVariable("ZOE_SOH");
            String pwr=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&&soh!=null&&pwr!=null&&temp!=null&&
               voltage!=null&&current!=null){

                StringBuilder tlm=new StringBuilder();
                tlm.append("{\"utc\":")
                   .append(System.currentTimeMillis()/1000L);
                tlm.append(",\"soc\":").append(soc);
                tlm.append(",\"soh\":").append(soh);
                tlm.append(",\"power\":").append(pwr);
                tlm.append(",\"batt_temp\":").append(temp);
                tlm.append(",\"voltage\":").append(voltage);
                tlm.append(",\"current\":").append(current);
                tlm.append(",\"is_charging\":")
                   .append(chargeNow?"1":"0");
                tlm.append(",\"is_dcfc\":0");

                boolean parked=(speedKmh>=0.0 && speedKmh<=1.0);

                tlm.append(",\"is_parked\":")
                   .append(parked?"1":"0");
                tlm.append("}");

                /* ABRP */
                HttpURLConnection ac=null;
                try{
                    URL u=new URL("https://api.iternio.com/1/tlm/send");
                    ac=(HttpURLConnection)u.openConnection();
                    ac.setRequestMethod("POST");
                    ac.setConnectTimeout(5000);
                    ac.setReadTimeout(5000);
                    ac.setDoOutput(true);
                    ac.setRequestProperty(
                        "Content-Type","application/json; charset=utf-8");
                    ac.setRequestProperty(
                        "Authorization",
                        "APIKEY 78f9e662-cbcc-4064-8fa9-1c8198564dd3");

                    String body="{\"tlm\":"+tlm.toString()+
                        ",\"token\":\"15e7573f-fa2b-49f8-a5dc-65014de88341\"}";

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

                    int code=ac.getResponseCode();
                    tasker.setVariable("ZOE_ABRP_HTTP_CODE",
                        String.valueOf(code));

                    InputStream ai=(code>=200&&code<400)?
                        ac.getInputStream():ac.getErrorStream();

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

                    tasker.setVariable("ZOE_ABRP_RESPONSE",resp);
                    tasker.setVariable("ZOE_ABRP_ERROR",
                        code>=200&&code<300?"":"HTTP "+code+": "+resp);

                }catch(Exception ae){
                    tasker.setVariable("ZOE_ABRP_ERROR",ae.toString());
                }finally{
                    if(ac!=null)ac.disconnect();
                }

                /* RED */
                HttpURLConnection rc=null;
                try{
                    URL u2=new URL("https://red.engles.fr/openapi/zoe");
                    rc=(HttpURLConnection)u2.openConnection();
                    rc.setRequestMethod("POST");
                    rc.setConnectTimeout(5000);
                    rc.setReadTimeout(5000);
                    rc.setDoOutput(true);
                    rc.setRequestProperty(
                        "Content-Type","application/json; charset=utf-8");
                    rc.setRequestProperty("Accept","application/json");

                    tasker.setVariable(
                        "ZOE_RED_SENT_JSON",tlm.toString());

                    OutputStream ro=rc.getOutputStream();
                    ro.write(tlm.toString().getBytes("UTF-8"));
                    ro.flush();
                    ro.close();

                    int code2=rc.getResponseCode();
                    tasker.setVariable("ZOE_RED_HTTP_CODE",
                        String.valueOf(code2));

                    InputStream ri=(code2>=200&&code2<400)?
                        rc.getInputStream():rc.getErrorStream();

                    String rr="";
                    if(ri!=null){
                        BufferedReader br2=new BufferedReader(
                            new InputStreamReader(ri,"UTF-8"));
                        String line2;
                        while((line2=br2.readLine())!=null)rr+=line2;
                        br2.close();
                    }

                    tasker.setVariable("ZOE_RED_RESPONSE",rr);
                    tasker.setVariable("ZOE_RED_ERROR",
                        code2>=200&&code2<300?"":"HTTP "+code2+": "+rr);
                    tasker.setVariable("ZOE_RED_LAST_SEND",
                        String.valueOf(System.currentTimeMillis()/1000L));

                }catch(Exception re){
                    tasker.setVariable("ZOE_RED_ERROR",re.toString());
                }finally{
                    if(rc!=null)rc.disconnect();
                }

                tasker.setVariable("ZOE_ABRP_POWER_SENT",pwr);
                tasker.setVariable("ZOE_ABRP_PERIOD",
                    String.valueOf(period/1000L));
                tasker.setVariable("ZOE_ABRP_LAST_SEND",
                    String.valueOf(System.currentTimeMillis()/1000L));
                tasker.setVariable("ZOE_ABRP_LAST_SEND_MS",
                    String.valueOf(System.currentTimeMillis()));
            }
        }

        success=true;
        tasker.setVariable("ZOE_LIVE_CONNECTED","1");
        tasker.setVariable("ZOE_LIVE_STATE",
            chargeNow?"Charging":"Live");
        tasker.setVariable("ZOE_LIVE_ERROR","");

    }catch(Exception e){

        lastError=e;

        tasker.setVariable(
            "ZOE_LIVE_ERROR",
            "READ attempt "+(attempt+1)+": "+
            e.getClass().getName()+": "+
            String.valueOf(e.getMessage()));

        try{
            Object s=tasker.getJavaVariable("zOE_SOCKET");
            if(s!=null)((BluetoothSocket)s).close();
        }catch(Exception ignored){}

        tasker.setJavaVariable("zOE_SOCKET",null);
        tasker.setVariable("ZOE_LIVE_CONNECTED","0");

        if(attempt==0)Thread.sleep(1000L);
    }
}

if(!success){
    tasker.setVariable("ZOE_LIVE_STATE","Disconnected");
    throw new Exception(
        "ZOE READ failed after reconnect: "+
        String.valueOf(lastError.getMessage()));
}
]]></Str>
            <Str sr="arg1" ve="3"/>
            <Str sr="arg2" ve="3"/>
            <Int sr="arg3" val="0"/>
        </Action>
    </Task>
</TaskerData>
