Tesla car charging with excess solar / Überschussladen

Hey folks, here’s my current setup to charge a Tesla car using the excess solar power, maybe it can be useful to others.

Here’s what it looks like in action:

Green: excess power, Red: power draw from grid (to be avoided), Yellow: heat pump, Blue: non-heat pump power, Purple: wallbox. Not shown is the overall PV production, which would be the sum of Green+Yellow+Blue+Purple minus any Red, so about 4kW on this day in February.

You can see the heat pump generating hot water around 11:00 and again around 15:00 and then turning off past 16:00.

The excess to the utility never approaches 2kW and instead all that gets funneled into the car. Shortly before 14:00 I changed the mode from normal to more aggressive, as I needed the car full the next day, so was willing to dip into the utility grid a bit.

Requirements:

  • OpenHab 4.x
  • solar inverter integrated into OH
  • so called “smart meter”, or a way to measure true excess at the utility handover point
  • a Tesla car
  • (optional) a power meter on the wall box (or on the UWC)

In my case it’s a Model 3, a Fronius Inverter, a Fronius SmartMeter and a Shelly Pro 3EM for the wallbox. The Shelly is optional and you can estimate the current draw of the car by reading the Tesla API, but that only has integer values for how many amps are being drawn and it can be delayed quite a bit.

With the bindings configured, these are the items we’ll need.

Number          Model3_ChargeMode "Lademodus [MAP(model3_chargemode.map):%s]" <none> (Tesla, g_ros)
Switch          Model3_Wakeup   "Wakeup" (Tesla) { channel="tesla:model3:4ec0e461:LW3E123456789:wakeup", expire="5m" }
Number          Model3_Battery  "Batterie [%d %%]" <batterylevel> (Tesla, g_ros) { channel="tesla:model3:4ec0e461:LW3E123456789:batterylevel" }
Switch          Model3_Charge   "Laden" (Tesla) { channel="tesla:model3:4ec0e461:LW3E123456789:charge", expire="5m" }
String          Model3_ChargeCable              "Ladekabel" (Tesla) { channel="tesla:model3:4ec0e461:LW3E123456789:chargecable" }
Switch          Model3_ChargeToMax              "Vollladen" (Tesla) { channel="tesla:model3:4ec0e461:LW3E123456789:chargetomax" }
Number:ElectricCurrent          Model3_ChargingAmps             "Ladestrom [%.1f A]" <energy> (Tesla) { channel="tesla:model3:4ec0e461:LW3E123456789:chargingamps", expire="5m" }
Number:ElectricCurrent          Model3_ChargerCurrent           "Ladestrom [%.1f A]" <energy> (Tesla) { channel="tesla:model3:4ec0e461:LW3E123456789:chargercurrent" }
Number:ElectricPotential        Model3_ChargerVoltage           "Ladespannung [%.1f V]" <energy> (Tesla) { channel="tesla:model3:4ec0e461:LW3E123456789:chargervoltage" }
Dimmer          Model3_ChargeLimit              "Ladelimit [%d %%]" <none> (Tesla) { channel="tesla:model3:4ec0e461:LW3E123456789:chargelimit" }
Location        Model3_Location "Location [%s]" (Tesla) { channel="tesla:model3:4ec0e461:LW3E123456789:location" }
DateTime        Model3_GpsTimestamp             "GPS Timestamp [%s]" (Tesla) { channel="tesla:model3:4ec0e461:LW3E123456789:gpstimestamp" }
    
Number:Power    PV_Excess "Überschuss Strom [%.0f W]" <solarplant> (g_solar)
Number:Power    WR_Grid_Power "Bezug EKZ [%.0f W]" <solarplant> (g_solar) { channel="fronius:powerinverter:132291b7:powerflowchannelpgrid", expire="5m" }
Number:Power    WR_Load_Power "Bezug Haus [%.0f W]" <solarplant> (g_solar) { channel="fronius:powerinverter:132291b7:powerflowchannelpload", expire="5m" }
Number:Power    WR_AC_Power   "WR Leistung [%.0f W]" <solarplant> (g_solar) { channel="fronius:powerinverter:132291b7:inverterdatachannelpac", expire="5m,state=0" }
    
Number:Power    PM_W_power "Wallbox Leistung [%.1f W]" <energy> (g_power) {channel="mqtt:topic:shellypro3em:total#act_power", unit="W" }

And here’s the rule that is triggered every minute (during the day) to adjust the power draw of the car.

rule "PV Excess"
when
    Item WR_AC_Power changed or
    Item WR_Grid_Power changed or
    Item WR_Load_Power changed
then
    var Number excess = WR_Load_Power.state
    if (WR_AC_Power.state != NULL) {
        val Number WR = WR_AC_Power.state
        excess += WR.doubleValue
    }     
    postUpdate(PV_Excess, excess)
end 

rule "Tesla_Auto_Laden"
when Time cron "0 */1 7-20 * * ?"                                                                                                                                                                                                                                                     
then
    // Not in PV-automatic mode, exit early.
    if (Model3_ChargeMode.state < 1 || Model3_ChargeMode.state > 2) {
        return
    }     
    // Set this to lat/long of the home geofence.
    val PointType home = new PointType("42.123456789, 23.123456789")
    if (Model3_Location.state == NULL) {
        logInfo("model3", "location state is null, aborting")
        return
    }     
    val distance = home.distanceFrom(Model3_Location.state as PointType)
    val staleness = ChronoUnit.MINUTES.between((Model3_GpsTimestamp.state as DateTimeType).getZonedDateTime, now)
   
    val cable = Model3_ChargeCable.state
    // Not plugged in ... don't even log anything.
    if (cable == "<invalid>") {
        return
    }
    // Not at home or location too stale.
    if (cable != "IEC" || distance > 100|m || staleness > 2|h) {
        logInfo("model3", "wrong cable {} or distance too big {} m or staleness too high {}", cable, distance, staleness)
        return
    }
    val limit = Model3_ChargeLimit.state
    val soc = Model3_Battery.state
    val to_max = Model3_ChargeToMax.state
    if (Model3_Battery.state != NULL && soc >= limit && !to_max) {
        logInfo("model3", "already charged to {} with limit {}", soc, limit)
        return
    }
    val pv = PV_Excess.state as Number
    val charging = Model3_Charge.state
    val current = Model3_ChargerCurrent.state
    val voltage = Model3_ChargerVoltage.state
    val meter = PM_W_power.state
    var int draw = 0
    if (charging == ON && cable == "IEC" && voltage > 200|V) {
        if (current >= 3|A && current <= 16|A) {
                draw = ((current as Number).floatValue * (voltage as Number).floatValue * 3).intValue
                logInfo("model3", "estimating current draw to {} vs meter {}", draw, meter)
        }
    }
    logInfo("model3", "excess is {} (+ {}W or + {}), charging is {}, cable is {}, current/voltage is {}/{}",
        pv, draw, meter, charging, cable, current, voltage)
   
    // Keep a 300W buffer
    // see https://tff-forum.de/t/pv-ueberschussladen-eines-model-3/109428/96 staying at 3A for long is probably stupid.
    var int buffer = 300
    // Allow a bit of utility draw, as we're in a rush
    if (Model3_ChargeMode.state == 2) {
        buffer = -300
    }
    // Can use `draw` here instead of `meter` to estimate without a metering device.
    var int to_charge = Math.floor((pv.floatValue + (meter as Number).floatValue - buffer) / (3 * 230)).intValue
    if (to_charge > 16) to_charge = 16
    if (to_charge >= 2) {
        if (charging != ON) {
            logInfo("model3", "turning on charging")
            Model3_Wakeup.sendCommand(ON)
            Model3_Charge.sendCommand(ON)
            Model3_ChargingAmps.sendCommand(to_charge)
        }
        if ((Model3_ChargingAmps.state as Number).intValue != to_charge) {
            logInfo("model3", "setting charge amps from {} to {}A", Model3_ChargingAmps.state as Number, to_charge)
            Model3_Wakeup.sendCommand(ON)
            Model3_ChargingAmps.sendCommand(to_charge)
            // TODO: if this was a reduction from a previous value, then don't
            // allow an increase in the next 10min or so, as it seems to be a
            // bit cloudy or whatever. Maybe have a blocking item with
            // expire=10m that we flip on and disallow increases based on that.
            // ditto for waiting a bit before turning it on, actually,
            // shouldn't increase just because of a hole in the cloud coverage
        }
    } else if (charging == ON) {
        logInfo("model3", "stopping charging, only have {}W excess", (pv+meter))
        Model3_Wakeup.sendCommand(ON)
        Model3_Charge.sendCommand(OFF)
    }
end
   
rule "Tesla_Stop_Laden"
when Item Model3_ChargeMode changed to 0
then
    Model3_Charge.sendCommand(OFF)
end
rule "Tesla_Manuell_Laden"
when Item Model3_ChargeMode changed to 5
then
    Model3_Wakeup.sendCommand(ON)
    Model3_Charge.sendCommand(ON)
end
rule "Tesla_Manuell_Laden_Timer"
when Item Model3_ChargeMode changed to 6
then
    Model3_Wakeup.sendCommand(ON)
    Model3_Charge.sendCommand(ON)
    createTimer(now.plusSeconds(4 * 60 * 60), [|
            Model3_Charge.sendCommand(OFF)
            Model3_ChargeMode.sendCommand(1)
    ])
end
rule "Tesla_Voll_Laden"
when Item Model3_ChargeMode changed to 9
then
    Model3_Wakeup.sendCommand(ON)
    Model3_Charge.sendCommand(ON)
    Model3_ChargeToMax.sendCommand(ON)
end
rule "Tesla_Voll_Laden_Beenden"
when Item Model3_Battery changed to 100
then
    Model3_ChargeMode.sendCommand(1)
end

Sitemap settings (transformations of the values elided):

    Frame label="Tesla" {
        Text item=Model3_Battery
        Switch item=Model3_ChargeMode mappings=[0="OFF", 1="PV", 2="PV+Utility", 5="ON", 6="ON (4h)", 9="FULL"] label=""
        Slider item=Model3_ChargingAmps step=1 minValue=2 maxValue=16
        Setpoint item=Model3_ChargeLimit step=5 minValue=15 maxValue=100
        Default item=Model3_Charge
        // TODO: add Charge input slider thingy to set to ON for user-selectable hours, then on cloudy day can set to 5h + 3A and walk away
        Group item=Tesla label="Details" icon="car"
    }

transform/model3_chargemode.map

0=OFF
1=PV
2=PV+EKZ
5=ON
6=ON (4h)
9=FULL
-=UNKNOWN

This also works fine without a wall box, using only the 1 phase wall plug charger (it’s what I did for several months), and that allows more fine grained adjustments in steps of +/- 230W. But there are several drawbacks to “trickle” charging the car this way.

Hope this helps!

4 Likes

This topic was automatically closed 41 days after the last reply. New replies are no longer allowed.