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!