Add support for Tesla Powerwall

FYI i got this loading with hassio, now I get the expected:

2020-03-19 16:22:33 WARNING (MainThread) [homeassistant.loader] You are using a custom integration for tesla_gateway which has not been tested by Home Assistant. This component might cause stability problems, be sure to disable it if you do experience issues with Home Assistant.

Not sure what was wrong (probably user error) so just removed all the files and did it again, did a restart and boom! (note the config checker still says it wont work till the restart)

Now i just need to play with it. Will report back soon.

@estebanp Finally got around to making my rules and i can set the mode between “backup” and “self_consumption”, changes quite quickly and works well. When i go back to Self consumption i set the backup_reserve_percent as well and it just seems to get ignored. Is this still working for you, my SW is 1.46.0?

This is what i send via the “call service” page for testing (guessing this is just wrong).

{
real_mode: 'self_consumption',
backup_reserve_percent: 5
}

Super helpful error back from Tesla:

Error 400 on call https://owner-api.teslamotors.com/api/1/energy_sites/XXXX/operation: {“response”:null,“error”:“Invalid operation setting for txid 9d1051fa667XXXXXXXXX50ab9d572cf”,“error_description”:“”}

@james_hiscott: regarding the warning, yeah, you will get that warning with any custom integration.

Regarding the error, at some point Tesla updated the API, is no longer “real_mode” but “default_real_mode”, that change should do the trick.

Doesn’t your code do that already:

        body = {
            'default_real_mode': service_data['real_mode'],
            'backup_reserve_percent':int(service_data['backup_reserve_percent'])
            }

It blows up if i pass:

image

You are right, can you enable the logs and send them? Make sure to hide the site id, user/password and other sensitive data.
Something like this in your configuration.yaml:

logger:
  logs:
    custom_components.tesla_gateway: debug
2020-06-08 17:35:22 DEBUG (MainThread) [custom_components.tesla_gateway] {'access_token': 'XXXXXXX', 'token_type': 'bearer', 'expires_in': 3888000, 'refresh_token': 'XXXXXXXX', 'created_at': 1591634122}

2020-06-08 17:35:22 DEBUG (MainThread) [custom_components.tesla_gateway] {'response': [{'energy_site_id': XXXXXX, 'resource_type': 'battery', 'site_name': 'Home Energy Gateway', 'id': 'XXXXXX-XXXXXX', 'gateway_id': 'XXXXXX-13-H--XXXXXXX', 'energy_left': 10405.78947368421, 'total_pack_energy': 14070, 'percentage_charged': 73.95728126285863, 'battery_type': 'ac_powerwall', 'backup_capable': True, 'battery_power': -130, 'sync_grid_alert_enabled': False, 'breaker_alert_enabled': False}], 'count': 1}

2020-06-08 17:35:22 DEBUG (MainThread) [custom_components.tesla_gateway] {'default_real_mode': 'self_consumption', 'backup_reserve_percent': 20}

2020-06-08 17:35:23 DEBUG (MainThread) [custom_components.tesla_gateway] set operation successful, response: {'response': {'code': 201, 'message': 'Updated'}}

2020-06-08 17:35:23 DEBUG (MainThread) [custom_components.tesla_gateway] revoke completed

Humm… looks like it works fine… but it never updates in the app. Still shows as 5% which is what i set it to manualy.

Check the state in the gateway’s local page (local IP of the gateway).

I have seen the App (iOS) taking a while to update, one time it didnt update for several hours, I ended up resetting the gateway. However, in my case no data was flowing, not even the production/consumption of the different parts.

No matter what i send as the backup_reserve_percent (5, 40,80) it always shows 8.8% on the local settings page, (even after leaving it at 40% for 12 hours) and in the app it shows 5% (which is what i set manually via the app)! All very strange… Is there anyway to check what the server thinks its set too instead of locally?

TBH I am not too worried as switching to “backup” sets it to 100% and gets it to draw from the grid, which is what to do when the electricity prices goes negative. (Basically we get paid to fill our battery)

Im getting the same error as James_hiscott:

Component error: tesla_gateway - Integration ‘tesla_gateway’ not found.

all files in the correct place, named correctly and correct content.

Tried the same action of removing all and recreating - no joy, still get the same error. Any ideas?

EDIT - now working, for anyone else following this, i put the changes required for configuration.yaml at the very bottom of the file, also if entering the username and password are in quotes " xxxx "

tesla_gateway:
username: "<username of your tesla account, can be through a secret>"
password: "<password of your tesla account, can be through a secret>"

@james_hiscott from the server the best bet is to use the phone app, you can also try https://www.teslaapi.io/powerwalls/state-and-settings.
Sorry, I dont change the percentage, so if there is some error there I am not aware. You can also try changing the format, maybe is expecting a float number (e.g. 0.05 for 5%) or it could be expecting a string with a percentage. Since is so hard to sniff the communication, we are pretty much guessing here what to send.

@Hodor glad you were able to figure it out! thanks for leaving the update.

I followed all the instructions for the custom_components directory, the component is being found (along with the usual custom component warning) but I am getting the same setup error:

2020-09-24 18:47:47 ERROR (MainThread) [homeassistant.setup] Setup failed for tesla_gateway: No setup function defined.

I verified the async_setup is present in the init.py code and I am running a very recent (0.115.1) version of HA.

@Matthew_Budraitis or anyone else, how did you go get past this?

Ok, the fix was renaming init.py to __init__.py. Hopefully that will help the next person :slight_smile:

More learnings: I can’t get the reserve percent to change, similar to @james_hiscott saw above. I am able to switch modes from self powered to backup-only and back, so things are basically wired up correctly. It could be a formatting issue as @estebanp mentioned on Jun 28.

I note that there is an implementation over at the SmartThings side of the world for controlling Powerwalls via the Tesla server APIs, but it uses the /api/1/energy_sites/:energySiteId/operation API only to set operation mode, and uses a separate /api/1/energy_sites/:energySiteId/backup API to set the backup reserve percent.

I’m going to muck around with @estebanp’s the code a bit to see if I can use the dedicated API call to make it work.

Ok I got this to work too, except the battery percentage piece but not too concerned with that.

I want to switch my PW to TOU at 10p and back to self at 6a during the summer when the AC is on. I set up two automations to do so, but when I hit the slider to turn them off, the slider turns back on within a second. What do?

The following two files are updated to support both operation mode changes and battery reserve percent changes.

All the other files are identical to the excellent post of @estebanp from above.

The first, services.yaml, needs to be put into /custom_components/tesla_gateway:

set_operation:
  description: >
    Changes operation mode
  fields:
    real_mode:
      description: Mode to set to the Tesla gateway.
      example: 'self_consumption, backup'
set_reserve:
  description: >
    Changes battery reserve percent in self_consumption mode
  fields:
    reserve_percent:
      description: Percentage of battery reserve
      example: 70

The second, __init__.py needs to go in the same directory:

"""
Monitors and controls the Tesla gateway.
"""
import logging

import aiohttp
import asyncio
import async_timeout
import json
import time
import voluptuous as vol

from homeassistant.const import (
    CONF_USERNAME,
    CONF_PASSWORD
    )
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv

DOMAIN = 'tesla_gateway'

_LOGGER = logging.getLogger(__name__)

DEFAULT_TIMEOUT = 100

CONFIG_SCHEMA = vol.Schema({
    DOMAIN: vol.Schema({
        vol.Required(CONF_USERNAME): cv.string,
        vol.Required(CONF_PASSWORD): cv.string,
    }),
}, extra=vol.ALLOW_EXTRA)

tesla_base_url = 'https://owner-api.teslamotors.com'

@asyncio.coroutine
def async_setup(hass, config):

    # Tesla gateway is SSL but has no valid certificates
    websession = async_get_clientsession(hass, verify_ssl=False)

    domain_config = config[DOMAIN]
    conf_user = domain_config[CONF_USERNAME]
    conf_password = domain_config[CONF_PASSWORD]
    access_token = None

    @asyncio.coroutine
    def login():

        login_url = tesla_base_url + '/oauth/token'
        headers = {'Content-type': 'application/json'}
        body = {
            'email': conf_user,
            'password': conf_password,
            'client_secret': 'c7257eb71a564034f9419ee651c7d0e5f7aa6bfbd18bafb5c5c033b093bb2fa3',
            'client_id': '81527cff06843c8634fdc09e8ac0abefb46ac849f38fe1e431c2ef2106796384',
            'grant_type': 'password'
        }

        try:
            with async_timeout.timeout(DEFAULT_TIMEOUT, loop=hass.loop):
                response = yield from websession.post(login_url,
                    headers=headers,
                    json=body,
                    raise_for_status=False)

            if response.status != 200:
                returned_text = yield from response.text()
                _LOGGER.warning('Error %d on call %s:\n%s', response.status, response.url, returned_text)
            else:
                returned_json = yield from response.json()
                _LOGGER.debug(returned_json)
                access_token = returned_json['access_token']
                return access_token

        except asyncio.TimeoutError:
            _LOGGER.warning('Timeout call %s.', response.url)

        except aiohttp.ClientError:
            _LOGGER.error('Client error %s.', response.url)

        return None

    @asyncio.coroutine
    def revoke(access_token):

        revoke_url = tesla_base_url + '/oauth/revoke'
        headers = {'Content-type': 'application/json'}
        body = {
            'token': access_token
        }

        try:
            with async_timeout.timeout(DEFAULT_TIMEOUT, loop=hass.loop):
                response = yield from websession.post(revoke_url,
                    headers=headers,
                    json=body,
                    raise_for_status=False)

            if response.status != 200:
                returned_text = yield from response.text()
                _LOGGER.warning('Error %d on call %s:\n%s', response.status, response.url, returned_text)
            else:
                _LOGGER.debug('revoke completed')
                return True

        except asyncio.TimeoutError:
            _LOGGER.warning('Timeout call %s.', response.url)

        except aiohttp.ClientError:
            _LOGGER.error('Client error %s.', response.url)

        return False

    @asyncio.coroutine
    def get_energy_site_id(access_token):

        list_url = tesla_base_url + '/api/1/products'
        headers = {
            'Authorization': 'Bearer ' + access_token
            }
        body = {}

        try:
            with async_timeout.timeout(DEFAULT_TIMEOUT, loop=hass.loop):
                response = yield from websession.get(list_url,
                    headers=headers,
                    json=body,
                    raise_for_status=False)

            if response.status != 200:
                returned_text = yield from response.text()
                _LOGGER.warning('Error %d on call %s:\n%s', response.status, response.url, returned_text)
            else:
                returned_json = yield from response.json()
                _LOGGER.debug(returned_json)
                for r in returned_json['response']:
                    if 'energy_site_id' in r:
                        return r['energy_site_id']
                return None

        except asyncio.TimeoutError:
            _LOGGER.warning('Timeout call %s.', response.url)

        except aiohttp.ClientError:
            _LOGGER.error('Client error %s.', response.url)

        return None

    @asyncio.coroutine
    def set_operation(access_token,energy_site_id,service_data):

        operation_url = tesla_base_url + '/api/1/energy_sites/{}/operation'.format(energy_site_id)
        headers = {
            'Content-type': 'application/json',
            'Authorization': 'Bearer ' + access_token
            }
        body = {
            'default_real_mode': service_data['real_mode']
            # 'backup_reserve_percent':int(service_data['backup_reserve_percent'])
            }
        _LOGGER.debug(body)

        try:
            with async_timeout.timeout(DEFAULT_TIMEOUT, loop=hass.loop):
                response = yield from websession.post(operation_url,
                    json=body,
                    headers=headers,
                    raise_for_status=False)

            if response.status != 200:
                returned_text = yield from response.text()
                _LOGGER.warning('Error %d on call %s:\n%s', response.status, response.url, returned_text)
            else:
                returned_json = yield from response.json()
                _LOGGER.debug('set operation successful, response: %s', returned_json)

        except asyncio.TimeoutError:
            _LOGGER.warning('Timeout call %s.', response.url)

        except aiohttp.ClientError:
            _LOGGER.error('Client error %s.', response.url)

    @asyncio.coroutine
    def async_set_operation(service):

        access_token = yield from login()
        if access_token:
            energy_site_id = yield from get_energy_site_id(access_token)
            if energy_site_id:
                yield from set_operation(access_token, energy_site_id, service.data)
            yield from revoke(access_token)

    hass.services.async_register(DOMAIN, 'set_operation', async_set_operation)

    @asyncio.coroutine
    def set_reserve(access_token,energy_site_id,service_data):

        operation_url = tesla_base_url + '/api/1/energy_sites/{}/backup'.format(energy_site_id)
        headers = {
            'Content-type': 'application/json',
            'Authorization': 'Bearer ' + access_token
            }
        body = {
            'backup_reserve_percent': int(service_data['reserve_percent'])
            }
        _LOGGER.debug(body)

        try:
            with async_timeout.timeout(DEFAULT_TIMEOUT, loop=hass.loop):
                response = yield from websession.post(operation_url,
                    json=body,
                    headers=headers,
                    raise_for_status=False)

            if response.status != 200:
                returned_text = yield from response.text()
                _LOGGER.warning('Error %d on call %s:\n%s', response.status, response.url, returned_text)
            else:
                returned_json = yield from response.json()
                _LOGGER.debug('set reserve successful, response: %s', returned_json)

        except asyncio.TimeoutError:
            _LOGGER.warning('Timeout call %s.', response.url)

        except aiohttp.ClientError:
            _LOGGER.error('Client error %s.', response.url)

    @asyncio.coroutine
    def async_set_reserve(service):

        access_token = yield from login()
        if access_token:
            energy_site_id = yield from get_energy_site_id(access_token)
            if energy_site_id:
                yield from set_reserve(access_token, energy_site_id, service.data)
            yield from revoke(access_token)

    hass.services.async_register(DOMAIN, 'set_operation', async_set_operation)
    hass.services.async_register(DOMAIN, 'set_reserve', async_set_reserve)

    return True

That’s it! One thing to note is that if you use these and the Tesla app doesn’t show any changes, try killing and restarting the app. The Tesla app caches old values and so even if the changes happen you won’t see them unless you exit and restart the app.

2 Likes

This is awesome and has helped me make some really good automations for the very dynamic time of use tariff we’re on here. However one thing that would be really good is if the powerwall could be queried for it’s current mode, backup percentage and remaining percentage using this same custom component. The latter is available direct from the gateway and I use that at the moment but it can be up to 5% out from what the app and web API report as the app displays remaining percentage hiding a buffer, but the gateway does not. This currently causes a response lag and sometimes small amounts of unwanted charging when I set the backup reserve to the current remaining value in order to pause discharge during cheap periods.

I noticed some folks said they could not find energy site ID, this is actually embedded in to your product list , so you have to get products first, filter for “battery” then pull out the attribute.

Some helpful code we wrote to assist:

This repo has the code we use to automate powerwall on rasberry pi.

Is it just me, or did the Powerwall firmware update 1.50.2 totally disable local API access?

Mine is on 1.50.1 and the official powerwall integration is still giving me live energy use stats. I don’t use local control though, preferring the app API calls in the custom integration above so i can’t say if local control is now completely dead.

Can you still get to the local page to view the status?

I am on 1.50.1 still so can’t confirm yet, but no doubt it will be alone soon.