Last active
November 27, 2021 07:48
-
-
Save adampetrovic/34eeefd3af3e055fb59b077eacab06a1 to your computer and use it in GitHub Desktop.
This file contains hidden or 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 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
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
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_number
s instead, which would allow me to change thresholds, min / max charging times etc.