Add support for Tesla Powerwall

Oh I legit thought I had broken something as this stopped working same time I updated my blinds, glad to see it’s not just me. I did reset the login on my powerwall but yeah the integration just fails at unexpected error.

The fix @nathanielh posted was not released yet. However, if you download that powerwall integration and place it on custom_components, you override the HA one. After resetting my PW passwords, I was able to connect (customer password) and I see the data now.

Thanks man I downloaded that repo and put it in my custom components, but when I try to run it I get this error

 [homeassistant.config_entries] Error occurred loading configuration flow for integration powerwall: No module named 'homeassistant.components.dhcp'

That may be related to some other component this one uses, seems like you need to enable this: https://www.home-assistant.io/integrations/dhcp/

Yep was the problem I updated my hass and now I get further but when I go to the integration I get this screen

image

This is the error in the log

2021-02-10 09:17:39 ERROR (MainThread) [custom_components.powerwall-master.config_flow] Unexpected exception
Traceback (most recent call last):
File "/config/custom_components/powerwall-master/config_flow.py", line 77, in async_step_user
info = await validate_input(self.hass, user_input)
File "/config/custom_components/powerwall-master/config_flow.py", line 40, in validate_input
site_info = await hass.async_add_executor_job(
File "/usr/local/lib/python3.8/concurrent/futures/thread.py", line 57, in run
result = self.fn(*self.args, **self.kwargs)
File "/config/custom_components/powerwall-master/config_flow.py", line 25, in _login_and_fetch_site_info
power_wall.login("", password)
File "/usr/local/lib/python3.8/site-packages/tesla_powerwall/powerwall.py", line 66, in login
return self.login_as(User.CUSTOMER, email, password, force_sm_off)
File "/usr/local/lib/python3.8/site-packages/tesla_powerwall/powerwall.py", line 59, in login_as
response = self._api.login(user, email, password, force_sm_off)
File "/usr/local/lib/python3.8/site-packages/tesla_powerwall/api.py", line 138, in login
return self.post(
File "/usr/local/lib/python3.8/site-packages/tesla_powerwall/api.py", line 128, in post
return self._process_response(response)
File "/usr/local/lib/python3.8/site-packages/tesla_powerwall/api.py", line 51, in _process_response
raise APIError(
tesla_powerwall.error.APIError: Powerwall api error: The url /api/login/Basic returned error 404

UPDATE:

Managed to fix the problem turns out the top box is ip address not the email login so all working now.

Hello everybody. I’m new to HO, so sorry if I have some stupid questions :wink:

I also added my Powerwall 2 into HO a few days ago. It also worked nearly 24 hours and I recorded data from 2021-02-07 20:38 until 2021-02-08 19:28. After that, no new data was recorded. Trying to play around with the integration did not help so I tried to remove and again add it. When I try to add it again, I get only the error “Unknown Error” after entering the IP address.

grafik

In addition, after trying to add the Tesla Powerwall integration, I get the following error in the log

Core Log
2021-02-10 19:59:08 ERROR (MainThread) [homeassistant.components.powerwall.config_flow] Unexpected exception
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/components/powerwall/config_flow.py", line 64, in async_step_user
    info = await validate_input(self.hass, user_input)
  File "/usr/src/homeassistant/homeassistant/components/powerwall/config_flow.py", line 27, in validate_input
    site_info = await hass.async_add_executor_job(power_wall.get_site_info)
  File "/usr/local/lib/python3.8/concurrent/futures/thread.py", line 57, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/usr/local/lib/python3.8/site-packages/tesla_powerwall/__init__.py", line 112, in get_site_info
    return SiteInfo(self._api.get_site_info())
  File "/usr/local/lib/python3.8/site-packages/tesla_powerwall/api.py", line 224, in get_site_info
    return self.get("site_info")
  File "/usr/local/lib/python3.8/site-packages/tesla_powerwall/api.py", line 103, in get
    return self._process_response(response)
  File "/usr/local/lib/python3.8/site-packages/tesla_powerwall/api.py", line 62, in _process_response
    raise AccessDeniedError(
tesla_powerwall.error.AccessDeniedError: Access denied for resource /api/site_info: Unable to GET to resource

Here is the content of various api sites of the Powerwall, I can access via the browser. Apparently, I’m running version 20.49.0. When trying to access any of the pages without logging in, I get a 403 error.

/api/site_info
  "max_system_energy_kWh": 0,
  "max_system_power_kW": 0,
  "site_name": "MY_NAME",
  "timezone": "Europe/Berlin",
  "max_site_meter_power_kW": 1000000000,
  "min_site_meter_power_kW": -1000000000,
  "nominal_system_energy_kWh": 27,
  "nominal_system_power_kW": 10,
  "grid_code": {
    "grid_code": "50Hz_230V_1_VDE4105_2018_Germany",
    "grid_voltage_setting": 230,
    "grid_freq_setting": 50,
    "grid_phase_setting": "Single",
    "country": "Germany",
    "state": "*",
    "distributor": "*",
    "utility": "*",
    "retailer": "*",
    "region": "VDE4105:2018"
/api/meters/site
[
  {
    "id": 0,
    "location": "site",
    "type": "neurio_mb",
    "cts": [
      true,
      true,
      true,
      false
    ],
    "inverted": [
      false,
      false,
      false,
      false
    ],
    "connection": {
      "serial_port": "/dev/ttymxc1",
      "baud": 115200,
      "modbus_id": 1,
      "short_id": "Tesla-0.0.7(2)",
      "device_serial": "0x000004714B017A8B",
      "neurio_connected": true,
      "disable_echo": true,
      "https_conf": {}
    },
    "real_power_scale_factor": 1,
    "Cached_readings": {
      "last_communication_time": "2021-02-10T19:34:14.290213152+01:00",
      "instant_power": 379.5035629272461,
      "instant_reactive_power": -855.8155517578125,
      "instant_apparent_power": 936.1854586058273,
      "frequency": 49.99971389770508,
      "energy_exported": 3204976.4024683805,
      "energy_imported": 1264833.5805239363,
      "instant_average_voltage": 234.78902689615884,
      "instant_total_current": 0,
      "i_a_current": 0,
      "i_b_current": 0,
      "i_c_current": 0,
      "v_l1n": 234.21629333496094,
      "v_l2n": 234.25747680664062,
      "v_l3n": 235.893310546875,
      "last_phase_voltage_communication_time": "2021-02-10T19:34:14.290204485+01:00",
      "real_power_a": 224.6822052001953,
      "real_power_b": 80.3097152709961,
      "real_power_c": 74.51164245605469,
      "reactive_power_a": -340.9933166503906,
      "reactive_power_b": -284.2757568359375,
      "reactive_power_c": -230.54647827148438,
      "last_phase_power_communication_time": "0001-01-01T00:00:00Z",
      "energy_exported_a": 1761675.663888889,
      "energy_exported_b": 1136651.7466666666,
      "energy_exported_c": 1907697.838611111,
      "energy_imported_a": 982981.7441666666,
      "energy_imported_b": 1684993.8566666667,
      "energy_imported_c": 197906.85861111112,
      "serial_number": "0x000004714B017A8B",
      "version": "Tesla-0.0.7(2)",
      "timeout": 1500000000
    }
  }
]
/api/meters/aggregates
{
  "site": {
    "last_communication_time": "2021-02-10T19:35:41.790690217+01:00",
    "instant_power": 374.6804389953613,
    "instant_reactive_power": -838.6568603515625,
    "instant_apparent_power": 918.5481809793631,
    "frequency": 49.99971389770508,
    "energy_exported": 3204976.4024683805,
    "energy_imported": 1264842.5433017144,
    "instant_average_voltage": 234.4982706705729,
    "instant_total_current": 0,
    "i_a_current": 0,
    "i_b_current": 0,
    "i_c_current": 0,
    "last_phase_voltage_communication_time": "0001-01-01T00:00:00Z",
    "last_phase_power_communication_time": "0001-01-01T00:00:00Z",
    "timeout": 1500000000
  },
  "battery": {
    "last_communication_time": "2021-02-10T19:35:41.790144887+01:00",
    "instant_power": -60,
    "instant_reactive_power": 10,
    "instant_apparent_power": 60.8276253029822,
    "frequency": 49.994,
    "energy_exported": 932880,
    "energy_imported": 1186530,
    "instant_average_voltage": 233.75,
    "instant_total_current": -0.1,
    "i_a_current": 0,
    "i_b_current": 0,
    "i_c_current": 0,
    "last_phase_voltage_communication_time": "0001-01-01T00:00:00Z",
    "last_phase_power_communication_time": "0001-01-01T00:00:00Z",
    "timeout": 1500000000
  },
  "load": {
    "last_communication_time": "2021-02-10T19:35:41.789851555+01:00",
    "instant_power": 325.2219996409865,
    "instant_reactive_power": -787.4931194493327,
    "instant_apparent_power": 852.0063158395733,
    "frequency": 49.99971389770508,
    "energy_exported": 0,
    "energy_imported": 3981514.2811111123,
    "instant_average_voltage": 234.4982706705729,
    "instant_total_current": 1.3868844265289435,
    "i_a_current": 0,
    "i_b_current": 0,
    "i_c_current": 0,
    "last_phase_voltage_communication_time": "0001-01-01T00:00:00Z",
    "last_phase_power_communication_time": "0001-01-01T00:00:00Z",
    "timeout": 1500000000
  },
  "solar": {
    "last_communication_time": "2021-02-10T19:35:41.789851555+01:00",
    "instant_power": 10.264152646064758,
    "instant_reactive_power": 44.61656951904297,
    "instant_apparent_power": 45.78199542603306,
    "frequency": 49.99971389770508,
    "energy_exported": 6175404.397503827,
    "energy_imported": 106.25722604848852,
    "instant_average_voltage": 234.61654154459634,
    "instant_total_current": 0,
    "i_a_current": 0,
    "i_b_current": 0,
    "i_c_current": 0,
    "last_phase_voltage_communication_time": "0001-01-01T00:00:00Z",
    "last_phase_power_communication_time": "0001-01-01T00:00:00Z",
    "timeout": 1500000000
  }
}
/api/status
{
  "start_time": "2021-02-08 19:37:02 +0800",
  "up_time_seconds": "48h0m2.672586969s",
  "is_new": false,
  "version": "20.49.0",
  "git_hash": "42eb71218f889a3aa3635b7b57cffe1de76b7438",
  "commission_count": 0,
  "device_type": "hec",
  "sync_type": "v1"
}

I would like to test the reworked code from your post by putting it into the init.py. But I cannot find the folder /usr/local/lib/python3.8/concurrent/futures/ nor usr/src/homeassistant/homeassistant/components/powerwall/. Maybe because I removed the integration before?

How could I get this working so I could test this on my site as well?

You need to download the component from https://github.com/bdraco/powerwall and place it in the custom_components folder, then restart HA
The component that is in the official HA release still doesnt have the fix

Thank you very much for your work :+1: It worked for me right now, at least I’m getting values again. Also I did not see any strange errors in the logs, so it seem to be fine for now.

So Im now completely lost.

I implemented the fix from bruceH5200 and this worked fine. Now today Ive lost access to the local sensors. I have tried to follow the thread from the last few days but Im really struggling to follow what has changed.

interestingly I have two IP addresses for my powerwall gateway, both are fixed. I used to access the local API using one ip address, which I assume is the one I set up the integration on initially months ago. However if I now use the other IP address I can access the API just fine via a browser.
I have tried finiding where this IP address is stored in the integration, but no luck.

I have also downloaded and installed the github custom component listed above and restarted HA, but still no luck.

Can anyone advise what needs to be done after the token fix posted by Bruce a couple of weeks ago to get this back working again?

Many thanks for any help.

The script doesn’t access the powerwall locally, it is talking to the Tesla servers via the API who in turn talk to the powerwall. So the IP address of the powerwall is not relevant.
The script still seems to be working for me with the hardcoded key.

I got the same error: ```
[homeassistant.config_entries] Error occurred loading configuration flow for integration powerwall: No module named ‘homeassistant.components.dhcp’


I added "dhcp:" to the config.yaml but then get an error "Component error: dhcp - Integration 'dhcp' not found."

I have default_config: in the config.yaml.

No idea how to get the powerwall component from bdraco working.

The changing mode from backup to self powered is still working using your fix, which works great.

However the sensors I had set up using the local gateway API have all stopped. I was pulling these from the API using local IP address (https://192.168.X.X/api/meters/aggregates) but as of this morning they have all dissappeared. Navigating to that address now fails.

If I change the IP address to another that is associated with the gateway, this works…

Ah - hang on. I wonder… I was working in the cabinet last night, I wonder if Ive gone and broken/loosened the network cable that is connected to the gateway and Im now polling by wifi…

This could be a big egg on face moment…

Has the firmware on your powerwall upgraded, I think may have been making security changes.

They must have been doing something, firmware hasnt updated, but a restart of the gateway has forced it to reconnect to the local network. Cable was fine, but gateway had lost its local IP address for some reason. Sensors all back up and running again now.

Still doesnt explain why the custom component is failing saying dhcp is not available when i have default config enabled. Seems I may have somethingunderlying wrong with my HA setup.

Some general clarification since there seem to be some confusion. There are two components:

  • tesla_gateway which is a hacky component I put together to change powerwall modes. Some time ago this component was using the local API (talking directly to the powerwall) but the handshake between Tesla and the PW changed and it became unstable to change mode (we would have to change it 3 times and stop/start the PW to actually change it). So instead, we went with thee public API which is the one that e.g. the phone apps use.
    This component stopped working recently because the authentication was changed to SSO. SSO is “single sign on” and is a method to sign you across multiple sites. In this method, you need to authenticate through a more complex handshake, then exchange tokens from the old method and continue. The SSO seems to complain if you try to sign on frequently, so we need to keep the token around and renew it (I am testing that change, is going well).
    Some of us use this component to optimize the times we have the powerwall on “backup” and “self_consumption” modes. In my case, I have a TOU energy plan with 3 different prices during the day. Times changes on weekends and holidays. To make it “cheaper” I set the powerwall on backup mode during super-offpeak and self_consumption otherwsie. I also calculate if I am not going to have enough energy to go through “on-peak” and reserve a % at off-peak. With this, I manager to consume almost never energy from the grid on “on-peak” and little on “off-peak”. This may depend on your system size and consumption profile.
    This component is just doing that, changing modes and setting the reserve %. Is NOT getting the consumption values.

  • powerwall is a component that was added some months ago to the official HA release. Me and others were using a rest sensor and talking directly to the powerwall. Seems you were doing the same. This was possible because the powerwall let you locally (using the local API) query these values without any authentication.
    Recently, Tesla updated the firmware and now that data is no longer available without authentication. The REST sensor doesn’t have things like templates for the headers, so you need to use a component to authenticate and get that data.
    Luckily, someone already wrote a component: powerwall. So, me and others have switched from using that rest sensor to this powerwall component. The powerwall component that is in the HA release still doesnt have this authentication. It will be there in the next release. To get it earlier, you can download it and place it on custom_components. HA then will override the one from HA with the one you have there. To configure this now you have to pass the local IP and the customer password.

For both of these authentications to work, the version of your PW has to be > 20.49.0

The dhcp issue seems to be related to some other setting/component in your HA configuration. There is a dhcp component: https://www.home-assistant.io/integrations/dhcp/
However, I dont have that enabled and dont hit that issue, but it could be related to my HA and network setup. I did not inspect the component to understand what that dhcp is used for. According to @jimmyleet, he updated his hass and that worked.
If that doesnt work, enable debug logging for that component and inspect the logs.

[quote=“Hodor, post:75, topic:142280”]
interestingly I have two IP addresses for my powerwall gateway, both are fixed.[/quote]
I also have two IP addresses, but one is for the LAN connection, and the other one is for the WLAN connection.

This worked fine for me. Copy the files into the newly created folder config/custom_components and restart HA. Then you can install the new integration powerwall. In the input form, enter the IP (upper field) and the password of the powerwall.

Is that true for all cases? When using the integration that came with my Home assistant image or the one suggestes by estebanp (GitHub - bdraco/powerwall: Updated powerwall integration for HASS), I had to enter the IP of my Powerwall.

Some smart people chiming in over on TMC about this change. Sharing in case some of the discussion there helps @estebanp.

1 Like

Sorry, I meant to do this sooner, didnt want to keep posting without testing it a bit more. This updated version has been working for a couple of days without any issues. Calling it frequently seems to work and so does calling it twice a day.
The local api is now working in the ‘Tesla Powerwall’ component from the public release of HA (https://www.home-assistant.io/integrations/powerwall/)
The owners API from the Tesla component is also working: https://www.home-assistant.io/integrations/tesla/
And here is the updated version of the Tesla gateway to modify the operation:

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

import aiohttp
import asyncio
import async_timeout
import base64
import hashlib
import json
import os
import re
import time
from urllib.parse import parse_qs
import voluptuous as vol

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

DOMAIN = 'tesla_gateway'
CONF_REFRESH_TOKEN = "refresh_token"

_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,
        vol.Optional(CONF_ACCESS_TOKEN, default=''): cv.string,
        vol.Optional(CONF_REFRESH_TOKEN, default=''): cv.string,
    }),
}, extra=vol.ALLOW_EXTRA)

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

step_max_attempts = 7
step_attempt_sleep = 3
TESLA_CLIENT_ID = '81527cff06843c8634fdc09e8ac0abefb46ac849f38fe1e431c2ef2106796384'

@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]

    @asyncio.coroutine
    def SSO_login():

        # Code extracted from https://github.com/enode-engineering/tesla-oauth2/blob/e6922c6e7805d9f65c109d90c5eaf806c5b71938/tesla.py
        # Login process explained at https://tesla-api.timdorr.com/api-basics/authentication

        authorize_url = tesla_auth_url + '/oauth2/v3/authorize'
        callback_url = tesla_auth_url + '/void/callback'

        headers = {
            "User-Agent": "curl",
            "x-tesla-user-agent": "TeslaApp/3.10.9-433/adff2e065/android/10",
            "X-Requested-With": "com.teslamotors.tesla",
        }
        
        verifier_bytes = os.urandom(86)
        code_verifier = base64.urlsafe_b64encode(verifier_bytes).rstrip(b"=")
        code_challenge = base64.urlsafe_b64encode(hashlib.sha256(code_verifier).digest()).rstrip(b"=").decode("utf-8")
        state = base64.urlsafe_b64encode(os.urandom(16)).rstrip(b"=").decode("utf-8")

        params = (
            ("client_id", "ownerapi"),
            ("code_challenge", code_challenge),
            ("code_challenge_method", "S256"),
            ("redirect_uri", callback_url),
            ("response_type", "code"),
            ("scope", "openid email offline_access"),
            ("state", state),
        )

        try:
            # Step 1: Obtain the login page
            _LOGGER.debug('Step 1: GET %s\nparams %s', authorize_url, params)
            for attempt in range(step_max_attempts):
                with async_timeout.timeout(DEFAULT_TIMEOUT, loop=hass.loop):
                    response = yield from websession.get(authorize_url,
                        headers=headers,
                        params=params,
                        raise_for_status=False)

                returned_text = yield from response.text()
                if response.status == 200 and "<title>" in returned_text:
                    crsf_regex_result = re.search(r'name="_csrf".+value="([^"]+)"', returned_text)
                    if crsf_regex_result:
                        _LOGGER.debug('Step 1: Success on attempt %d', attempt)
                        break

                _LOGGER.warning('Step 1: Error %d on attempt %d, call %s:\n%s', response.status, attempt, response.url, returned_text)
                time.sleep(step_attempt_sleep)
            else:
                raise ValueError('Step 1: failed after %d attempts, last response %s:\n%s', step_max_attempts, response.status, returned_text)

            # Step 2: Obtain an authorization code
            csrf = crsf_regex_result.group(1)
            transaction_id = re.search(r'name="transaction_id".+value="([^"]+)"', returned_text).group(1)

            body = {
                "_csrf": csrf,
                "_phase": "authenticate",
                "_process": "1",
                "transaction_id": transaction_id,
                "cancel": "",
                "identity": conf_user,
                "credential": conf_password,
            }
            
            _LOGGER.debug('Step 2: POST %s\nparams: %s\nbody: %s', authorize_url, params, body)
            for attempt in range(step_max_attempts):
                with async_timeout.timeout(DEFAULT_TIMEOUT, loop=hass.loop):
                    response = yield from websession.post(authorize_url,
                        headers=headers,
                        params=params,
                        data=body,
                        raise_for_status=False,
                        allow_redirects=False)

                returned_text = yield from response.text()

                if "We could not sign you in" in returned_text and response.status == 401:
                    raise ValueError('Step 2: Invalid credentials. Error %d on call %s:\n%s', response.status, response.url, returned_text)

                if response.status == 302 or "<title>" in returned_text:
                    _LOGGER.debug('Step 2: Success on attempt %d', attempt)
                    break

                _LOGGER.warning('Step 2: Error %d on call %s:\n%s', response.status, response.url, returned_text)
                time.sleep(step_attempt_sleep)
            else:
                raise ValueError('Step 2: failed after %d attempts, last response %s:\n%s', step_max_attempts, response.status, returned_text)

            is_mfa = True if response.status == 200 and "/mfa/verify" in returned_text else False
            if is_mfa:
                raise ValueError('Multi-factor authentication enabled for the account and not supported')
            
            # Step 3: Exchange authorization code for bearer token
            code = parse_qs(response.headers["location"])[callback_url + '?code']

            token_url = tesla_auth_url + '/oauth2/v3/token'
            body = {
                "grant_type": "authorization_code",
                "client_id": "ownerapi",
                "code_verifier": code_verifier.decode("utf-8"),
                "code": code,
                "redirect_uri": callback_url
            }

            _LOGGER.debug('Step 3: POST %s', token_url)
            with async_timeout.timeout(DEFAULT_TIMEOUT, loop=hass.loop):
                response = yield from websession.post(token_url,
                    headers=headers,
                    data=body,
                    raise_for_status=False)

            returned_json = yield from response.json()
            access_token = returned_json['access_token']
            domain_config[CONF_ACCESS_TOKEN] = access_token
            domain_config[CONF_REFRESH_TOKEN] = returned_json['refresh_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 SSO_refresh_token():
        token_oauth2_url = tesla_auth_url + '/oauth2/v3/token'
        headers = {
            "User-Agent": "curl",
            "x-tesla-user-agent": "TeslaApp/3.10.9-433/adff2e065/android/10",
            "X-Requested-With": "com.teslamotors.tesla",
        }
        body = {
            "grant_type": "refresh_token",
            "refresh_token": domain_config[CONF_REFRESH_TOKEN],
            "client_id": "ownerapi",
            "scope": "openid email offline_access",
        }
        with async_timeout.timeout(DEFAULT_TIMEOUT, loop=hass.loop):
            response = yield from websession.post(token_oauth2_url,
                headers=headers,
                data=body,
                raise_for_status=False)
        returned_json = yield from response.json()
        access_token = returned_json['access_token']
        domain_config[CONF_ACCESS_TOKEN] = access_token
        domain_config[CONF_REFRESH_TOKEN] = returned_json['refresh_token']
        return access_token

    @asyncio.coroutine
    def OWNER_get_token(access_token):
        try:
            token_oauth_url = tesla_base_url + '/oauth/token'
            headers = {
                "User-Agent": "curl",
                "authorization": "bearer " + access_token,
            }
            body = {
                "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
                "client_id": TESLA_CLIENT_ID,
            }
            with async_timeout.timeout(DEFAULT_TIMEOUT, loop=hass.loop):
                response = yield from websession.post(token_oauth_url,
                    headers=headers,
                    data=body,
                    raise_for_status=False)
            returned_json = yield from response.json()
            owner_access_token = returned_json["access_token"]
            return owner_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 OWNER_revoke(owner_token):
        revoke_url = tesla_base_url + '/oauth/revoke'
        headers = {'Content-type': 'application/json'}
        body = {
            'token': owner_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(owner_token):
        list_url = tesla_base_url + '/api/1/products'
        headers = {
            'Authorization': 'Bearer ' + owner_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()
                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(owner_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 ' + owner_token
            }
        body = {
            'default_real_mode': service_data['real_mode'],
            'backup_reserve_percent':int(service_data['backup_reserve_percent'])
            }
        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 get_owner_api_token():
        access_token = domain_config[CONF_ACCESS_TOKEN]
        if not access_token:
            access_token = yield from SSO_login()
        else:
            access_token = yield from SSO_refresh_token()
        if not access_token:
            return None
        owner_token = yield from OWNER_get_token(access_token)
        return owner_token
 
    @asyncio.coroutine
    def async_set_operation(service):
        owner_token = yield from get_owner_api_token()
        if owner_token:
            energy_site_id = yield from get_energy_site_id(owner_token)
            if energy_site_id:
                yield from set_operation(owner_token, energy_site_id, service.data)
            yield from OWNER_revoke(owner_token)

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

    return True

Hi @estebanp,

I updated the Tesla Powerwall component from HA integrations and got that set up. I now have the PW data back in Lovelace.

I updated my __init__.py with the code from your last post. It changed the mode one time but then fails on any subsequent attempt. HA log shows:

2021-02-17 19:25:44 ERROR (MainThread) [homeassistant.core] Error executing service: <ServiceCall tesla_gateway.set_operation (c:0946cb13e5ebb5c883491eda478b757f): real_mode=backup>
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/core.py", line 1471, in catch_exceptions
    await coro_or_task
  File "/usr/src/homeassistant/homeassistant/core.py", line 1490, in _execute_service
    await handler.job.target(service_call)
  File "/config/custom_components/tesla_gateway/__init__.py", line 281, in async_set_operation
    headers=headers,
  File "/config/custom_components/tesla_gateway/__init__.py", line 106, in login
    _LOGGER.warning('Step 1: Error %d on attempt %d, call %s:\n%s', response.status, attempt, response.url, returned_text)
NameError: name 'returned_test' is not defined

Any suggestions?
Thanks!

Looks like I had a typo on that line, it should be ‘returned_text’, just fixed the last post. However, this is showing that you are getting an issue on the first step… if you fix that, can you post the log? check that it doesnt have anything sensitive