Last active
November 27, 2021 07:48
-
-
Save adampetrovic/34eeefd3af3e055fb59b077eacab06a1 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | |
}) |
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
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
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