Objective

When I got my electric car, I set it up to charge overnight during off-peak times, from 10 PM to 6 AM. I kept this schedule even after we installed a solar power system with a yearly net-metering contract, which basically meant it didn’t matter economically when I charged the car. Our solar panels, along with about 40,000 other household setups in Slovenia, are sending electricity back into the grid. Considering the infrastructure, environment, and self-sufficiency, it just made sense to use our own solar energy for charging.

Charging directly with solar power means using the energy as soon as it was produced, rather than pulling it from the grid. But in the winter, the amount of solar energy we could generate each day might not be enough, which meant we needed a way to charge the car that wouldn’t put too much strain on the grid.

I wanted to automate charging so it will charge with exactly the same power as it is produced. The main challenge was variability in daily solar energy, especially on partly cloudy days when the power could be jumping between 1000W and 8000W. The automation needed to detect such changes and adapt the charging power accordingly.

Hardware

The solar setup is monitored through a SolarEdge SE3K-SE10K inverter, which sends data including energy output and various electrical metrics via Modbus to Home Assistant.

The Tesla Model Y is charged using a Mobile Connector, which supports charging currents from 1A to 13A at 230V. I recorded the actual power consumption at different currents: 1A -> 215W
2A -> 460W
3A -> 670W
4A -> 850W
5A -> 1050W
6A -> 1290W
7A -> 1500W
8A -> 1700W
9A -> 1900W
10A -> 2100W
11A -> 2340W
12A -> 2550W
13A -> 2730W

Tesla is integrated with Home Assistant through an API that allows remote control of car’s features, including toggling charging on or off, and setting the charging current. Both of those were crucial for implementation of charging logic.

Automation Logic

The charging logic involves:

  1. Adjusting the charging current between 1A and 13A based on the currently generated solar power (ranging from 0 to 10,000W).
  2. Charging at the lowest electricity rate if the battery level falls below 30%.

Using Home Assistant’s Template Helper, I created a translation between the solar output and the coresponding charging currents. This generated a sensor that indicates the appropriate current level from 1 to 13 based on real-time production.

Template sensor is configured like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{% set solar_power = states('sensor.solaredge_ac_power') | float %}
{% if solar_power < 275 %}
  {{ "0" }}
{% elif solar_power > 275 and solar_power < 510 %}
  {{ "1" }}
{% elif solar_power > 510 and solar_power < 720 %}
  {{ "2" }}
{% elif solar_power > 720 and solar_power < 900 %}
  {{ "3" }}
{% elif solar_power > 900 and solar_power < 1100 %}
  {{ "4" }}
{% elif solar_power > 1100 and solar_power < 1340 %}
  {{ "5" }}
{% elif solar_power > 1340 and solar_power < 1550 %}
  {{ "6" }}
{% elif solar_power > 1550 and solar_power < 1750 %}
  {{ "7" }}
{% elif solar_power > 1750 and solar_power < 1950 %}
  {{ "8" }}
{% elif solar_power > 1950 and solar_power < 2150 %}
  {{ "9" }}
{% elif solar_power > 2150 and solar_power < 2390 %}
  {{ "10" }}
{% elif solar_power > 2390 and solar_power < 2600 %}
  {{ "11" }}
{% elif solar_power > 2600 and solar_power < 2780 %}
  {{ "12" }}
{% else %}
  {{ "13" }}
{% endif %}

The following picture displays the solar production (below) alongside the calculated charging currents, visualized by a colored band above the production graph.

Then I developed an automation that remotely adjusts the charging current in real-time based on these calculations, impelemnted in the change_charging_amps() function. The schedule_force_charge_low_battery() function ensures the car charges during the least expensive time if the battery is low.

I have also setup voice notifications for the start of charging and reminders to plug in the charger.

Users have the option to deactivate the automation, for example, before a long trip, and it only functions when the car is at home to avoid unintended changes at public chargers.

To take full control over charging, I first had to disable automatic charging that is controlled from Tesla mobile app. The only way to prevent automatic charging is to schedule it. But because eventually the scheduled time will be reached, the automation should rechedule charging just before the current scheduled time is reached. This is implemented in the prevent_autocharging() function, which sets the charging schedule eight hours back every 8 hours. With that, the car always enticipates charging, but never actually reaches it.

Lastly, I developed a tool that can determine the upcoming cheapest electricity tariff (Slovenia only) which is available on GitHub: ElektroTarife.

Full Appdaemon app:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import appdaemon.plugins.hass.hassapi as hass
from datetime import datetime, timedelta
import pytz


CHARGE_AMPS_ENTITY = 'number.lj_19_pei_charging_amps'
CHARGING_SWITCH_ENTITY = 'switch.lj_19_pei_charger'
SCHEDULING_API =  {'path_vars': {'vehicle_id': 929860842796599},
                   'enable': True,
                   'time': 0,
                   'wake_if_asleep': True}

class TeslaCharging(hass.Hass):
    def initialize(self):
        self.announcements = self.get_app('announcements')
        self.max_charge_amps = int(self.get_state('sensor.max_tesla_charge_amps_based_on_soar_generation'))
        self.low_battery = self.get_state('binary_sensor.tesla_low_battery')
        self.initial_charging_eval(entity='', attribute='', old='', new='', kwargs={})
        self.listen_state(self.change_charging_amps, 'sensor.max_tesla_charge_amps_based_on_soar_generation')
        self.listen_state(self.schedule_force_charge_low_battery, 'binary_sensor.tesla_low_battery')
        self.listen_state(self.manual_mode_change, 'input_boolean.tesla_manual_charging')
        self.listen_state(self.initial_charging_eval, 'binary_sensor.lj_19_pei_charger', new='on')
        
    def change_charging_amps(self, entity, attribute, old, new, kwargs):
        self.max_charge_amps = int(new)
        if self.get_state('device_tracker.lj_19_pei_location_tracker') != 'home':
            return
        charger_plugged_in = self.get_state('binary_sensor.lj_19_pei_charger') # charger plugged in
        if self.max_charge_amps == 0 and charger_plugged_in == 'on':
            self.stop_charging({})
        elif old == '0':  # means it is started to charge
            if charger_plugged_in == 'on':
                self.start_charging(amps=self.max_charge_amps, announce=True)
            else: 
                self.announcements.instant_announcement_message(message='Pojdi priključit avto.')
        else: # just amp update
            self.start_charging(amps=self.max_charge_amps, announce=False)
        
    def schedule_force_charge_low_battery(self, entity, attribute, old, new, kwargs):
        self.low_battery = new
        if self.is_manual() or new == 'off':
            return
        tw = self.get_app('time_windows')
        next_cheapest = tw.next_cheapest()
        cheapest_start_dt = next_cheapest.start
        cheapest_stop_dt = next_cheapest.stop
        
        now_dt = datetime.now(pytz.timezone('Europe/Ljubljana')) 
        if cheapest_start_dt < now_dt and cheapest_stop_dt > now_dt:
            self.log('Attempting to charge due to low battery')
            self.start_charging_low_battery({})
        else:
            self.run_at(self.start_charging_low_battery, cheapest_start_dt)
            self.log(f'Scheduled charging at {next_cheapest.start} due to low battery')        
        self.run_at(self.stop_charging, cheapest_stop_dt)

    def start_charging_low_battery(self, kwargs):
        '''First checks if the battery is still low. This is not necessarily the case, 
        because it might got charged using solar before next cheap time window'''
        self.low_battery = self.get_state('binary_sensor.tesla_low_battery')
        if self.low_battery == 'off':
            self.log('Battery not low anymore')
            return
        self.start_charging(amps=13, announce=True)
        
    def start_charging(self, amps, announce=False):
        if self.is_manual():
            return
        self.call_service('switch/turn_on', entity_id=CHARGING_SWITCH_ENTITY)
        self.call_service('number/set_value', entity_id=CHARGE_AMPS_ENTITY, value=amps)
        if announce is True:
            self.announcements.instant_announcement_message(message='Začenjam s polnenjem avta.') 

    def stop_charging(self, kwargs):
        if self.is_manual():
            return
        self.call_service('switch/turn_off', entity_id=CHARGING_SWITCH_ENTITY)
        self.call_service('number/set_value', entity_id=CHARGE_AMPS_ENTITY, value=0)

    def prevent_autocharging(self, kwargs):
        now_dt = datetime.now(pytz.timezone('Europe/Ljubljana'))
        eight_hours_ago = now_dt - timedelta(hours=8)
        eight_hours_ago_minutes_from_midnight = eight_hours_ago.hour * 60 + eight_hours_ago.minute
        next_exec = eight_hours_ago + timedelta(hours=24) - timedelta(minutes=10)
        self.schedule_charging_at(minutes_from_midnight=eight_hours_ago_minutes_from_midnight)
        self.run_at(self.prevent_autocharging, next_exec)
            
    def schedule_charging_at(self, minutes_from_midnight):
        if self.is_manual():
            return
        SCHEDULING_API['enable'] = True
        SCHEDULING_API['time'] = minutes_from_midnight
        self.call_service('tesla_custom/api',
                          command='SCHEDULED_CHARGING',
                          parameters=SCHEDULING_API)
    
    def is_manual(self):
        ''' Allows user to turn off this automation from user interface
            Also do not use automation if car is not home (when charging on public chargers)
        ''' 
        if self.get_entity('input_boolean.tesla_manual_charging').get_state() == 'on':
            return True
        if self.get_state(entity_id='device_tracker.lj_19_pei_location_tracker') != 'home':
            return True
        return False
    
    def manual_mode_change(self, entity, attribute, old, new, kwargs):
        if new == 'on':
            SCHEDULING_API['enable'] = False
            self.call_service('tesla_custom/api',
                              command='SCHEDULED_CHARGING',
                              parameters=SCHEDULING_API)
        elif new == 'off':
            self.initial_charging_eval(entity='', attribute='', old='', new='', kwargs={})
            
    def initial_charging_eval(self, entity, attribute, old, new, kwargs):
        self.prevent_autocharging({})
        self.log('Charging started manually')
        if self.max_charge_amps != 0:
            self.log(f'Starting to charge with {self.max_charge_amps}A')
            self.change_charging_amps(entity='', attribute='', old='0', new=self.max_charge_amps, kwargs={})
        if self.low_battery == 'on':
            self.log('Battery is low, scheduling charging..')
            self.schedule_force_charge_low_battery(entity='', attribute='', old='', new='', kwargs={})