Tesla + SolarEdge



How to Use

This example obtains the solar production from SolarEdge and change the charging current in Tesla. The goal is to charge the Tesla from solar production, not from the grid. It will check in 10 minutes interval.

To run it:

  • You need to download and install Total Control 9.0 (Update 40) Beta 3 or official release.
  • You need Professional version, install Lite version, you have 1-time offer to try Professional free for 3 days.
  • Copy-n-paste the following script and paste into a file, assuming it is "/tmp/charging.js'.
  • Change the values in the "settings".
  • Open script terminal, and enter sigmaLoad('/tmp/charging.js'). The script will open mySolarEdge, get production, calculate the amp based on values in settings and set the correct current in Tesla app.
  • Create an empty file (or any file), assuming it is named "/tmp/Afile".
  • Open command prompt cd to "/tmp" directory, "copy Afile CheckNow" to check solar production again (otherwise it will wait until settings.waitInterval minutes). "copy Afile StopNow" to stop the script execution.



Source Code

/*

This is an example, it checks the solar production from SolarEdge application and adjust Tesla charging current, 
it will stop when the production (minus reserve) reaches below "minAmp" or non-zero retriesBeforeStop is reached. 
The idea is to charge Tesla with solar energy, not from the grid.

Version: 0.1

*/

var controls = {};
var settings = {
    // How many voltage per Amp
    voltagePerAmp: 245,
    // Lower than this Amp, will exit if retriesBeforeStop is reached
    minAmp: 6,
    // Maximum Amp Tesla can accept
    maxAmp: 32,
    // Wattage to reserve for other purposes
    reserve: 500,
    // Check every x minutes
    waitInterval: 10,
    // Number of failed retries before stop, zero will not stop
    retriesBeforeStop: 0,
    // Number of zero before exit (e.g. sunset)
    retriesOnZero: 3,
    // Number of restart on error
    retriesRestart: 3,
    // Display interaction
    display: true
}

function padZero(str, length) {
    length = length || 2;
    return str.toString().padStart(length, '0');
}

function getLocalDateTimeWithAmPm() {
    const now = new Date();
    const hours = now.getHours();
    const amPm = hours >= 12 ? 'PM' : 'AM';
    const hours12 = hours % 12 || 12; 
    return now.getFullYear() + '-' + padZero(now.getMonth() + 1) + '-' + padZero(now.getDate()) + ' ' +
        padZero(hours12) + ':' + padZero(now.getMinutes()) + ":" + 
        padZero(now.getSeconds()) + ' ' + amPm;

}

function xprint(msg) {
    print(getLocalDateTimeWithAmPm(new Date()) + " : " + msg);
}

const fileExists = require('fs').exists;
const removeFile = require('fs').remove;
function xsleep(sleepTime) {
    const sleepInterval = 1000;
    var curTime = 0;
    while (curTime < sleepTime) {
        sleep(sleepInterval);
        curTime += sleepInterval;
        if (fileExists('/tmp/StopNow')) {
            removeFile('/tmp/StopNow')
            exitNow(0,"Stopped by user");
        }
        if (fileExists('/tmp/CheckNow')) {
            removeFile('/tmp/CheckNow');
            return;
        }
    }
}

function sendAai(obj, ignoreError) {
    ignoreError = ignoreError || false;
    var retval = getDevice().sendAai(obj);
    if (retval == null) {
        errorStr = "Error on: " + JSON.stringify(obj) + ":" + lastError();
        if (ignoreError) {
            xprint(">> " + errorStr + ". Ignored");
            return null;
        } 
        throw new Error(errorStr);
    }
    if (settings.display) {
        xprint(">>" + JSON.stringify(obj) + "\n" + JSON.stringify(retval));
    }
    return retval;
}

function setCharging(newState) {
    openTesla();
    const pat = "(Start|Stop) Charging";
    const retval = sendAai({action:`newQuery('T:/${pat}/', 2000);getIds;getText`}, true);
    if (retval == null) {
        throw new Error("Cannot obtain the charging status pattern");
    }
    const id = retval.list[1].ids[0];
    const chargingString = retval.list[2].retval;
    const regExp = new RegExp(pat);
    const groups = chargingString.match(regExp);
    if (groups == null) {
        throw new Error("Invalid charging string");
    }
    // Action is reverse, "Start Charging" current is not charging.
    const curState = groups[1] == "Start" ? false : true; 
    if (curState == newState) {
        return;
    }
    sendAai({elements:[id], action:"click"});
    const text = (newState ? "Start" : "Stop") + " Charging";
    return sendAai({action:`waitQuery(T:${text}, 3000)`}, true) != null;
}

var lastErrorMsg = "";
function setError(msg) {
    lastErrorMsg = msg;
}

function getError() {
    return lastErrorMsg;
}

function getSolarProduction() {
    const retval = sendAai({actions:[
        "openApp(mySolarEdge)", 
        "waitQuery(T:Lifetime,25000)", 
        "newQuery(R:.pv_power_now)", 
        "getText",
        "sendKey(Back)", 
        "sendKey(Back)"]});
    // getText
    const prodStr = retval.list[3].retval;
    const groups = prodStr.match(/^(\d+(\.?\d+)?) (W|kW)/);
    if (groups == null) {
        setError("Cannot find pattern in mySolarEdge");
        return -1;
    }
    const unit = groups.splice(-1);
    var number = groups[1];
    if (unit == "kW") {
        number = number * 1000;
    } else if (unit != "W") {
        setError("Unknown unit: " + unit);
        return -1;
    }
    return Math.round(number);
}

function startCharging() {
    return setCharging(true);
}

// Some time stop Charging will take effect on 2nd or 3rd click
function stopCharging() {
    return setCharging(false);
}

function adjAmp(num) {
    openTesla();
    var ctrl;
    var count;
    // Sometime the "<" or ">" is not fully recognized, run several times to make
    // sure the number match
    for (var i = 0; i < 5; ++i) {
        var amp = sendAai({elements:[controls.amp], action:"getText"}).retval.split(" ")[0];
        if (amp == num) {
            return true;
        } else if (amp > num) {
            ctrl = controls.less;
            count = amp - num;
        } else {
            ctrl = controls.more;
            num = Math.min(settings.maxAmp, num);
            count = num - amp;
        }
        sendAai({elements:[ctrl], action:`repeat(${count},clickWait)`});
    }
}

function init() {
    // May take 20-30 seconds to bring up the charging information 
    var success = sendAai({action:"restartApp(Tesla, 'T:/(Start|Stop) Charging/', 30000)"}, true);
    if (success == null) {
        exitNow(2, "Cannot find charging information, please make sure app in charging mode");
    }
    var retval = sendAai({query:"T:/[0-9]+ A/&&TX"});
    if (retval.count != 3) {
        exitNow(3, "Cannot locate controls");
    }
    controls.less = retval.ids[0];
    controls.more = retval.ids[2];
    controls.amp = retval.ids[1];
    var solarProd = getSolarProduction();
    if (solarProd == -1) {
        exitNow(4, "Cannot obtain solar production: " + getError());        
    }
    // Create a function "clickWait"
    sendAai({action:"function(clickWait)", clickWait:["sleep(200)", "nsClick"]});
    return true;
}

function start() {
    var retriesCounter = 0;
    var retriesOnZero = 0;
    while (true) {
        var solarProd = getSolarProduction();
        if (solarProd == -1) {
            throw new Error("Cannot get solar production");
        }
        if (solarProd == 0 && settings.retriesOnZero != 0) {
            retriesOnZero++;
            if (retriesOnZero >= settings.retriesOnZero) {
                exitNow(1, "Zero production");
            }
        }
        // .7 or above will carry to next integer
        var amp = Math.floor((solarProd - settings.reserve)/settings.voltagePerAmp + 0.3);
        amp = Math.min(amp, settings.maxAmp);
        amp = amp < 0 ? 0 : amp;
        xprint("Receive solar production: " + solarProd + " Watts, adjust to " + amp + " A");
        if (amp < settings.minAmp) {
            retriesCounter++;
            if (retriesCounter < settings.retriesBeforeStop || settings.retriesBeforeStop == 0) {
                stopCharging();
                xprint("Current is too low, " + amp + " A. Retrying next interval. Waiting...");
            }
        } else {
            startCharging();
            retriesCounter = 0;
            adjAmp(amp);
        }
        xsleep(settings.waitInterval*60*1000);
    }
}

function exitNow(code, msg) {
    xprint(msg);
    xprint("Exit code: " + code);
    // last attempt to stop charging
    try {stopCharging()} catch (e) {};
    exit(code);
}

if (device == null) {
    throw new Error("No device is connected");
}

const getDevice = () => device;
const openTesla = () => sendAai({action:"openApp(Tesla);sleep(300)"});

var retriesRestart = 0;
while(true) {
    try {
        init();
        start();
    } catch (e) {
        bypassExit(e);
        retriesRestart++;
        if (retriesRestart >= settings.retriesRestart) break;
        xprint("Error found: " + e);
        xprint("Restart the script");
    }
}
TCHelp