Cilj

Na začetku sem polnenje električnega avta nastavil na statični urnik, vsak dan v času nizke tarife (med 22:00 in 6:00). Kasneje smo postavili sončno elektrarno z letnim neto (net-metering) obračunavanjem ampak urnika polnenja nisem spremenil. Z net-metering pogodbo je vsaj z ekonomskega vidika čisto vseeno, kdaj ga polnim.

S takim polnilnim urnikom proizvedena energija s strehe ni nikoli prišla do baterije avta, ampak ven v elektro omrežje istočasno kot 40.000 drugih slovenskih malih solarnih elektrarn. Z vidika infrastrukture, okolja in samooskrbnosti, se mi je zato zdelo smiselno polniti avto z lastno energijo.

S tem bom sproti porabljal proizvedeno energijo in tako za polnenje ne bom jemal iz elektro omrežja. Ker pa (predvidevam) predvsem pozimi ne bo dovolj proizvedene energije za napolnit avto vsak dan, sem moral najti še način, da se bo avto v tem primeru polnil v času, ko je elektro omrežje najmanj obremenjeno.

Ena izmed težav, ki sem jih odkril ob implementaciji je bila spremenljivost prozivedene energije skozi dan. To je bilo predvsem opazno ob delno oblačnih dnevih, ko je proizvodnja skakala med 1000W in 8000W (nazivna moč naše elektrarne je 10kW). Želel sem, da polnenje sledi proizvodnji, torej, če elektrarna proizvaja 2000W, bi se moral avto polniti s podobno močjo in se nato prilagoditi, če pride do spremembe v proizvodnji.

Strojna oprema

Monitoring 10kW sončne elektrarne izvajam preko inverterja SolarEdge SE3K-SE10K, ki podatke pošilja lokalno preko protokola Modbus v Home Assistant. Podatki vključujejo prozivedeno energijo, moč, tokove in napetosti za vse faze in še ene par stvari, ki jih ne razumem.

Tesla Model Y se polni preko Mobile Connector, ki baterijo polne z napetostjo 230V in tokom med 1A in 13A. Za potrebe implementacije sem zmeril še dejansko porabljeno moč pri posamezni nastavitvi polnenja:
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

Integracija avta s Home Asisstant je izvedena preko API, ki med drugim omogoča nastavitev polnilnega toka in vklop ter izklop polnenja. Vse to se uporabil za implementacijo polnilne logike.

Implementacija logike

Logika:

  1. Nastavi in vzdržuj največji možen polnilni tok (1A - 13A) glede na trenutno moč sončne elektrarne (0-10,000W).
  2. Če baterija pade pod 30%, nastavi polnenje v času najnižjega tarifnega bloka.

Glede na meritve zgoraj sem najprej pripravil translacijo med proizvodnjo energije in največjim polnilnim tokom, ki pa ne sme preseči proizvodne moči. To sem naredil z pomočjo Template Helper v Home Assistant. Rezultat je virtualni senzor, ki vrne številko od 1 do 13. To je polnilni tok, ki bo kasneje uporabljen za nastavitev polnenja avta.

Nastavitve Template virtualnega senzorja:

 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 %}

Virtualni senzor ob boku proizvodni moči ob koncu dneva, ko proizvodnja pada. Barvni pas zgoraj prikazuje različne vrednosti primernega polnilnega toka glede na proizvodnjo na grafu spodaj:

Kar je ostalo je razvoj programa, ki v realnem času zazna spremembo primernega polnilnega toka in ga posreduje avtu, da prilagodi polnenje. To implementira funkcija change_charging_amps(), medtem ko schedule_force_charge_low_battery() poskrbi za nastavitev polnenja v najcenejšem časovnem bloku v primeru nizke napolnjenosti baterije (če elektrarna ne dela veliko oziroma avta ni bilo doma takrat). Dodatno sem nastavil še glasovna obvestila, ko se polnenje začne ali če smo pozabili vključiti polnilec.

Uporabniki imamo možnost enostavnega izklopa programa, če bi na primer želeli avto polniti za dlje časa pred daljšim potovanjem. Poleg tega, program deluje samo, ko je avto doma. S tem preprečim neželjene spremembe polnilnega toka med polnenjem na javnih polnilnicah.

Da pa bi Home Assistant res popolnoma nadziral polnenje, sem moral preprečiti samodejno polnenje glede na nastavitve v Teslini aplikaciji. Edini način, da se Tesla ne bo začela polniti ob vključitvi polnilca je z nastavitvijo urnika polnenja (čas začetka polnenja). Z nekaj sreče mi je uspelo dinamično spreminjati čas urnika tako, da nikoli ne bo dosežen. To implementira funkcija prevent_autocharging(), ki nastavi čas začetka polnenja 8 ur nazaj od trenutnega časa. To povzroči, da se avto v pričakovaju nastavljenega začetka ne bo začel samodejno polniti. Ko pa se trenutni čas približa nastavljenemu času, bo funkcija zopet prestavila čas začetka 8 ur nazaj in proces se začne znova.

Pomemben del programa je določitev najcenejšega prihajajočega časovnega bloka, takrat je elektro omrežje najmanj obremenjeno. Za ta namen se pripravil orodje, ki olajša delo z novimi tarifami. Na voljo na Githubu

Celotni program za Appdaemon, ki upravlja polnenje avta:

  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.tesla_charging_amps'
CHARGING_SWITCH_ENTITY = 'switch.tesla_charger'
SCHEDULING_API =  {'path_vars': {'vehicle_id': 00000000000000},
                   '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.tesla_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.tesla_location_tracker') != 'home':
            return
        charger_plugged_in = self.get_state('binary_sensor.tesla_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.tesla_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={})