Using Tesla Model 3 / Model S Battery Pack for Solar and monitoring with Home Assistant

EVTV (evtv.me) did some amazing work in building a Controller and BMS that can connect directly to Tesla Model S or Model 3 battery modules. This allows us to check voltage right down to the specific cell level.

They recently gave me the UDP structure that are in just encapsulated CAN frames, and I would like to figure out how to read UDP packets in HA (while I have spent the last 5 days searching the forums and Google, I am likely missing something rudimentary).

I am new to Home Assistant, and over the last two weeks have come to absolutely LOVE it! I now have it controlling which chargers (3.3kW or 6.6kW or both) to switch on based on solar levels reported from my Enphase Envoy (using 12V Relays and Shelly devices). It even controls the trigger of an ATS that switches to inverting power from my 75kWh model 3 battery pack using retired Danfoss DLX solar inverters (cheap) when the sun goes down.

Below is the structure, please can someone point me in the right direction

Thanks
Wayne


The UDP packets sent back and forth are really encapsulated CAN frames. This was done because we were previously using CAN and still can do so. But, those same packets are sent over UDP for wireless comm. I’ve pasted below the code within the Raspberry Pi application that interprets the incoming frames. I’m also attaching the way it takes the UDP traffic and turns it into a socketcan frame:

frame.can_id = udpFrame[8] + (udpFrame[9] << 8ul) + (udpFrame[10] <<

16ul) + (udpFrame[11] << 24ul);

for (int c = 0; c < 8; c++)

{

frame.data[c] = udpFrame[c];

}

So, basically, the first 8 bytes are the data payload, then 4 bytes for the ID. Each UDP CAN frame is 24 bytes long.

Here is the original CAN_FRAME structure members (in order) for reference:

BytesUnion data; // 64 bits - lots of ways to access it.

uint32_t id; // 29 bit if ide set, 11 bit otherwise uint32_t fid; // family ID - used internally to library uint32_t timestamp; // CAN timer value when mailbox message was received.

uint8_t rtr; // Remote Transmission Request (1 = RTR, 0 = data frame) uint8_t priority; // Priority but only important for TX frames and then only for special uses (0-31) uint8_t extended; // Extended ID flag uint8_t length; // Number of data bytes

And here is the interpretation of the "CAN" traffic from UDP:

switch (frame->can_id)

{

//status frames

case 0x150: //pack current, voltage, AH, hi/lo cell V //Bytes 0/1 LSB/MSB Pack Current x1.

//Bytes 2/3 LSB/MSB Pack Voltage rounded //Bytes 4/5 LSB/MSB Pack ampere-hours x 10 //Byte 6 Highest Cell Temperature //Byte 7 Lowest Cell Temperature // tempCurrent = (frame->data[0] + frame->data[1] * 256); tempCurrent = (frame->data[1]<<8 | frame->data[0]); status.current = tempCurrent; status.voltage = (frame->data[2] + frame->data[3] * 256) / 10.0f; tempAH = (frame->data[5]<<8 | frame->data[4]); status.AHUsed = config.ahUsed = fabs(tempAH / 10.0f); status.maxTemp = frame->data[6]; status.minTemp = frame->data[7]; if (bDebugging) printf("Cur: %f, V: %f AH: %f\n", status.current, status.voltage, status.AHUsed); break; case 0x650:

//byte 0 = State of Charge (0-100)

//bytes 1/2 Internal resistance (not really used) //byte 3 - Pack health (0-100) not really used //byte 4-5 - Pack OCV //byte 6 - Total cycles status.SOC = frame->data[0] / 2.0f; if (bDebugging) printf("SOC = %f\n", status.SOC); break; case 0x651:

//Bytes 0-1 Lowest cell V in thousandths //bytes 2-3 Highest cell V in thousandths //bytes 4-5 Average cell V in tenths //byte 6 = Max # of cells (255) //byte 7 = Actual # of cells status.minV = (frame->data[0] + frame->data[1] * 256) / 1000.0f; status.maxV = (frame->data[2] + frame->data[3] * 256) / 1000.0f; status.avgV = (frame->data[4] + frame->data[5] * 256) / 1000.0f; if (status.minV > status.maxV || status.avgV > 5.0f) { status.minV = 2.0; status.maxV = 2.0; status.avgV = 2.0; } if (bDebugging) printf("Min: %f, Max: %f, Avg: %f\n", status.minV, status.maxV, status.avgV); break; case 0x652:

//Bytes 0-1 Max charge current (1A incs) //bytes 2-3 Max discharge current //bytes 4-5 max cell V * 10 //bytes 6-7 min cell V * 10 //doesn't seem to be any need to parse this frame ID status.vMAX = (frame->data[4] + frame->data[5] * 256) / 10.0f; status.vMIN = (frame->data[6] + frame->data[7] * 256) / 10.0f; break; case 0x653:

enphase.numInverters = (frame->data[0] + frame->data[1] * 256); enphase.productionPower = (frame->data[2] + frame->data[3] * 256); enphase.productionToday = (frame->data[4] + frame->data[5] * 256) / 100.0f; enphase.productionWeek = (frame->data[6] + frame->data[7] * 256) / 100.0f; cyclesSinceEnvoy = 0; break; case 0x68F:

//Byte 0 = Sequence # (in this case the module #) //Byte 1 = First temperature of module //Byte 2 = First cell V - 2 and * 100 //Bytes 3-7 = Same as 2 but second through sixth cell moduleIdx = frame->data[0]; if (moduleIdx >= MAX_MODULES) moduleIdx = MAX_MODULES - 1; //indexes start at 0

//printf("\nModuleIdx: %d\n", moduleIdx); status.moduleCount = frame->data[1]; if (status.moduleCount > MAX_MODULES) status.moduleCount = MAX_MODULES; status.modules[moduleIdx].minV = 6.0f; status.modules[moduleIdx].maxV = 0.0f; status.modules[moduleIdx].avgV = 0.0f; for (int i = 0; i < 6; i++) { thisV = (frame->data[i+2] / 100.0f) + 2.0f; status.modules[moduleIdx].voltages[i] = thisV; //printf("This V: %f\n", thisV); status.modules[moduleIdx].avgV += thisV; if (thisV < status.modules[moduleIdx].minV) { status.modules[moduleIdx].minV = thisV; } if (thisV > status.modules[moduleIdx].maxV) { status.modules[moduleIdx].maxV = thisV; } } status.modules[moduleIdx].avgV /= 6.0f; break; //configuration frames case 0x680:

//Byte 0 = # of parallel strings

//byte 1 = Tenths of a second precharge (85 = 8.5 seconds) //bytes 2-3 = Variance allowed between cells * 100 //bytes 4-5 = HIgh V cutoff * 100 //bytes 6-7 = Low V cutoff * 100 config.numParallelStrings = frame->data[0]; config.prechargeTime = frame->data[1] / 10.0; config.varianceAllowed = (frame->data[2] + frame->data[3] * 256) / 100.0; config.hiVCutoff = (frame->data[4] + frame->data[5] * 256) / 100.0; config.lowVCutoff = (frame->data[6] + frame->data[7] * 256) / 100.0; break; case 0x681:

//bytes 0-1 = Charge cutoff V * 100

//bytes 2-3 = Charge resume V * 100

//bytes 4-5 = High Temp Limit * 10

//bytes 6-7 = Low temp limit * 10

config.chargeCutoffV = (frame->data[0] + frame->data[1] * 256) / 100.0; config.chargeResumeV = (frame->data[2] + frame->data[3] * 256) / 100.0; config.hiTempLimit = (frame->data[4] + frame->data[5] * 256) / 10.0; config.lowTempLimit = (frame->data[6] + frame->data[7] * 256) / 10.0; break; case 0x682:

//bytes 0-1 = Amp hours (used)

//bytes 2-3 = Pack capacity in AH

//bytes 4-5 = CAN0 Speed / 1000

//bytes 6-7 = CAN1 Speed / 1000

config.ahUsed = status.AHUsed = fabs((frame->data[0] + frame->data[1] * 256)); config.packCapacity = (frame->data[2] + frame->data[3] * 256); config.can0Speed = (frame->data[4] + frame->data[5] * 256); config.can1Speed = (frame->data[6] + frame->data[7] * 256); break; case 0x685:

//byte 0 = Sequence #

//bytes 1-7 = Part of SSID string

memcpy ((char *)(config.SSID - 7 + (frame->data[0] * 7)), &frame->data[1], 7); break; case 0x686:

//byte 0 = Sequence #

//bytes 1-7 = part of password

memcpy ((char *)(config.password - 7 + (frame->data[0] * 7)), &frame->data[1], 7); break; case 0x687:

//Byte 0 = Sequence #

//Bytes 1-7 = Part of time zone specifier memcpy ((char *)(config.timezone - 7 + (frame->data[0] * 7)), &frame->data[1], 7); break; }

I got this working in two different ways:

  1. NodeRed to cut up the packets received across UDP
  2. Then I built a CANBus over wifi using and ESP8266 and read the values directly

Works really well.

Can you post some more info on how you got the NodeRed to parse the packets on the UDP port ?

Any chance you could post the detail of how you got this to work…would enjoy trying on my setup.

Can you post some more info on how you got the