Skip to content

Instantly share code, notes, and snippets.

@adampetrovic
Last active November 27, 2021 07:48
Show Gist options
  • Save adampetrovic/34eeefd3af3e055fb59b077eacab06a1 to your computer and use it in GitHub Desktop.
Save adampetrovic/34eeefd3af3e055fb59b077eacab06a1 to your computer and use it in GitHub Desktop.
from dateutil import parser, tz
from datetime import datetime, timezone
MAX_TIME_RANGE = 12 * 60 # how many minutes in the future to look at forecasts
BATTERY_CAPACITY = 82 # kWh - Tesla Model 3 Performance (2021)
CHARGE_RATE = 11 # kWh - Gen 3 Wall Connector (3 Phase 16A)
LOCAL_TZ = tz.gettz('Australia/Sydney')
def get_price_forecast():
# initialise prices with current price
prices = [state.getattr(sensor.amber_general_price)]
forecasts = sorted(sensor.amber_general_forecast.forecasts, key=lambda x: parser.parse(x['start_time']))
times_seen = 0
while len(forecasts) and (times_seen < MAX_TIME_RANGE):
forecast = forecasts.pop(0)
times_seen += forecast['duration']
prices.append(forecast)
return prices
def format_charge_spots(charge_spots):
spots = []
for spot in charge_spots:
spots.append({
'start_time': parser.parse(spot['start_time']).astimezone(LOCAL_TZ),
'end_time': parser.parse(spot['end_time']).astimezone(LOCAL_TZ),
'per_kwh': spot['per_kwh'],
})
return spots
@state_trigger("sensor.amber_general_forecast", "binary_sensor.tesla_plugged_in == 'on'") # trigger: if our price forecast changes
@state_active("input_boolean.tesla_optimal_charging == 'on' and device_tracker.venom == 'home'") # condition: car is at home
def cheapest_charging_strategy(**kwargs):
charge_time_remaining = 0.0
# is the car drawing power from the charger?
if float(sensor.tesla_power) == 0.0:
# need to calculate remaining charge time ourselves.
current_battery = float(sensor.tesla_battery_level)
desired_battery = float(sensor.tesla_charge_limit_soc)
charge_time_remaining = (((((desired_battery - current_battery) / 100) * BATTERY_CAPACITY) / CHARGE_RATE) * 60)
else:
charge_time_remaining = float(sensor.tesla_time_to_full_charge) * 60
# do we actually need to charge the car?
if charge_time_remaining <= 0:
state.set('sensor.tesla_charging_strategy', value='stopped', new_attributes={
'next_start_time': None,
'next_end_time': None,
'reason': 'fully charged',
})
return
# sort prices by lowest $/kwh, prioritising earlier slots over later slots
prices = sorted(get_price_forecast(), key=lambda x: (float(x['per_kwh']), parser.parse(x['start_time'])))
charge_spots = []
allocated_charge_time = 0.0
for price in prices:
allocated_charge_time += price['duration']
charge_spots.append(price)
# we have enough slots, stop.
if allocated_charge_time >= charge_time_remaining:
break
# sort charge spots by earliest time
charge_spots = sorted(charge_spots, key=lambda x: parser.parse(x['start_time']))
start_time = parser.parse(charge_spots[0]['start_time'])
end_time = parser.parse(charge_spots[0]['end_time'])
kwh = charge_spots[0]['per_kwh']
# are we currently in a selected slot?
if start_time < datetime.now(timezone.utc) <= end_time:
switch.venom_charger_switch.turn_on()
if sensor.tesla_charging_strategy == 'stopped':
notify.phones(title="Tesla Charger",
message="Charging Started. Cost {}c/kWh.".format(kwh))
state.set('sensor.tesla_charging_strategy', value='started', new_attributes={
'charge_time_remaining': charge_time_remaining,
'next_start_time': start_time.astimezone(LOCAL_TZ),
'next_end_time': end_time.astimezone(LOCAL_TZ),
'price_kwh': kwh,
'reason': 'cheap',
'slots': format_charge_spots(charge_spots),
})
else:
switch.venom_charger_switch.turn_off()
if sensor.tesla_charging_strategy == 'started':
notify.phones(title="Tesla Charger",
message="Charging Stopped. Cost {}c/kWh. Starting again at {}".format(kwh, start_time.astimezone(LOCAL_TZ).strftime("%-I:%M%p").lower()))
state.set('sensor.tesla_charging_strategy', value='stopped', new_attributes={
'charge_time_remaining': charge_time_remaining,
'next_start_time': start_time.astimezone(LOCAL_TZ),
'next_end_time': end_time.astimezone(LOCAL_TZ),
'price_kwh': sensor.amber_general_price,
'reason': 'too expensive',
'slots': format_charge_spots(charge_spots),
})
@Steveveepee
Copy link

Hi Adam,

I'm super keen to see where this project goes. I had/have a similar proof of concept going with node-red, but would prefer to code it in python.

What's the state machine framework that this is built for? Would you like some (amateur) help with this?

Thx

@adampetrovic
Copy link
Author

Hey Steve. It's using pyscript which is a custom component of the fantastic HomeAssistant project.

Feel free to fork it and have a play. This does what I need it to for the time being, but I was hoping to extend out the configuration parameters to be more user configurable through the HomeAssistant. e.g. the globals at the top of this file could be input_numbers instead, which would allow me to change thresholds, min / max charging times etc.

@J-Rod-16
Copy link

J-Rod-16 commented Nov 2, 2021

Hi Adam

Trying to set up something similar for my model 3. Are you controlling the on/off through the charger or the car itself? I can see the venom_charger_switch reference but not sure what this is.

Thanks
Jarrad

@adampetrovic
Copy link
Author

Hi @J-Rod-16

This is a python script that uses HomeAssistant’s pyscript integration as an execution environment. It’s not intended to be used as a standalone script.

The charger switch is an entity that gets exposed when you setup the Tesla Custom Integration (https://github.com/alandtse/tesla) that controls the state of charging through Tesla’s vehicle API

@J-Rod-16
Copy link

J-Rod-16 commented Nov 2, 2021

Thanks for the quick reply. I hadn’t see that Tesla Custom Integration before. I’ll try it out once I get my Home Assistant set up on a raspberry pi.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment