This forum uses cookies
This forum makes use of cookies to store your login information if you are registered, and your last visit if you are not. Cookies are small text documents stored on your computer; the cookies set by this forum can only be used on this website and pose no security risk. Cookies on this forum also track the specific topics you have read and when you last read them. Please confirm whether you accept or reject these cookies being set.

Help with Tesla Powerwall API V3
#1
Tesla recently changed its api to use v3 of the Tesla auth endpoint.

I've seen the V1 example script for logic machine, however the auth process seems a bit more complicated now!

An explanation of the auth process can be found here: https://tesla-api.timdorr.com/api-basics/authentication  and here https://teslamotorsclub.com/tmc/threads/...st-5308136

I've seen an example script written in Python

https://github.com/fkhera/powerwallCloud...up.py#L132

Is anyone able to help me convert to lua to run on logic machine.


Specifically I'd like to automate a change in mode from 'backup-only' (which forces the Powerwall to charge from the Grid when rates are cheap) to 'self-powered' (which discharges the battery to power our home).

Many thanks in advanced

Kind Regards
James
Reply
#2
follow +1
Reply
#3
If you can provide username/password via PM then I can create an example.
Reply
#4
Many thanks!
Reply
#5
This should get you a request token and refresh token which can be used for further API calls. But this can stop working anytime if the login page structure changes since this example emulates user interaction with the login page.
Code:
username = 'user@domain.com'
password = 'password'
client_id = 'ID'
client_secret = 'SECRET'

encdec = require('encdec')
http = require('socket.http')
mime = require('mime')
ltn12 = require('ltn12')
json = require('json')

function mt()
  local ts, tu = os.microtime()
  return ts .. '.' .. tu
end

function b64url(str)
  return mime.b64(str):gsub('.', {
    ['+'] = '-',
    ['/'] = '_',
    ['='] = '',
  })
end

function encodeargs(t)
  local res = {}
  local esc = require('socket.url').escape

  for k, v in pairs(t) do
    res[ #res + 1 ] = esc(k) .. '=' .. esc(v)
  end

  return table.concat(res, '&')
end

code_verifier = encdec.sha512(mt()):sub(1, 86)
state = b64url(encdec.sha256(mt()):sub(1, 12))
code_challenge = b64url(code_verifier)

args = encodeargs({
  client_id = 'ownerapi',
  code_challenge = code_challenge,
  code_challenge_method = 'S256',
  redirect_uri = 'https://auth.tesla.com/void/callback',
  response_type = 'code',
  scope = 'openid email offline_access',
  state = state,
})

url = 'https://auth.tesla.com/oauth2/v3/authorize?' .. args
res, code, headers = http.request(url)
if not res or code ~= 200 then
  log('request 1 failed', res, code)
  return
end

postdata = {}
regexp = '<input type="hidden" name="([^"]+)" value="([^"]*)"'

for name, value in res:gmatch(regexp) do
  postdata[ name ] = value
end

postdata.identity = username
postdata.credential = password

cookie = headers['Set-Cookie'] or headers['set-cookie'] or ''
body = encodeargs(postdata)

res, code, headers = http.request({
  url = url,
  method = 'POST',
  source = ltn12.source.string(body),
  headers = {
    ['Content-Type'] = 'application/x-www-form-urlencoded',
    ['Content-Length'] = #body,
    ['Cookie'] = cookie,
  }
})

if not res or code ~= 302 then
  log('request 2 failed', res, code)
  return
end

hdr = headers.Location or headers.location
resp_code = hdr:match('code=([^&]+)')

body = json.encode({
  grant_type = 'authorization_code',
  client_id = 'ownerapi',
  code = resp_code,
  code_verifier = code_verifier,
  redirect_uri = 'https://auth.tesla.com/void/callback',
})

resp = {}

res, code, headers = http.request({
  url = 'https://auth.tesla.com/oauth2/v3/token',
  method = 'POST',
  source = ltn12.source.string(body),
  sink = ltn12.sink.table(resp),
  headers = {
    ['Content-Type'] = 'application/json',
    ['Accept'] = 'application/json',
    ['Content-Length'] = #body,
    ['User-Agent'] = 'Mozilla/5.0 (Linux; Android 9.0.0; VS985 4G Build/LRX21Y; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.83 Mobile Safari/537.36',
    ['X-Tesla-User-Agent'] = 'TeslaApp/3.4.4-350/fad4a582e/android/9.0.0',
  }
})

if not res or code ~= 200 then
  log('request 3 failed', res, code)
  return
end

resp = table.concat(resp)
resp = json.pdecode(resp)

bearer_token = resp.access_token
refresh_token = resp.refresh_token

body = json.encode({
  grant_type = 'urn:ietf:params:oauth:grant-type:jwt-bearer',
  client_id = client_id,
  client_secret = client_secret,
})

resp = {}

res, code, headers = http.request({
  url = 'https://owner-api.teslamotors.com/oauth/token',
  method = 'POST',
  source = ltn12.source.string(body),
  sink = ltn12.sink.table(resp),
  headers = {
    ['Content-Type'] = 'application/json',
    ['Authorization'] = 'Bearer ' .. bearer_token,
    ['Content-Length'] = #body,
  }
})

print(res, code)
if not res or code ~= 200 then
  log('request 4 failed', res, code)
  return
end

resp = table.concat(resp)
resp = json.pdecode(resp)

access_token = resp.access_token
log(access_token)
Reply
#6
Hi

Many thanks for the example. I've loaded it onto my Schneider SHAC (which runs an older version of the logic machine os) however I get the following error

script:36: attempt to call field 'sha512' (a nil value)

Kind Regards
James
Reply
#7
Try replacing these lines:
Code:
code_verifier = encdec.sha512(mt()):sub(1, 86)
state = b64url(encdec.sha256(mt()):sub(1, 12))
code_challenge = b64url(code_verifier)

With this:
Code:
rnd = encdec.sha256(mt())
code_verifier = (rnd .. rnd):sub(1, 86)
state = b64url(rnd:sub(1, 12))
code_challenge = b64url(code_verifier)
Reply
#8
Hi .. The script progresses a little further with this change, however still errors

Can you suggest what else I should update?

* arg: 1
* string: request 1 failed
* arg: 2
* nil
* arg: 3
* string: error:1409442E:lib(20):func(148):reason(1070)

Kind Regards
James
Reply
#9
This is due to outdated libraries in your firmware. Unfortunately it's not possible to update these libraries separately.
See if this works for you:
Code:
username = 'user@domain.com'
password = 'password'
client_id = 'ID'
client_secret = 'SECRET'

encdec = require('encdec')
http = require('socket.http')
mime = require('mime')
ltn12 = require('ltn12')
json = require('json')

function mt()
  local ts, tu = os.microtime()
  return ts .. '.' .. tu
end

function b64url(str)
  return mime.b64(str):gsub('.', {
    ['+'] = '-',
    ['/'] = '_',
    ['='] = '',
  })
end

function encodeargs(t)
  local res = {}
  local esc = require('socket.url').escape

  for k, v in pairs(t) do
    res[ #res + 1 ] = esc(k) .. '=' .. esc(v)
  end

  return table.concat(res, '&')
end

rnd = encdec.sha256(mt())
code_verifier = (rnd .. rnd):sub(1, 86)
state = b64url(rnd:sub(1, 12))
code_challenge = b64url(code_verifier)

args = encodeargs({
  client_id = 'ownerapi',
  code_challenge = code_challenge,
  code_challenge_method = 'S256',
  redirect_uri = 'https://auth.tesla.com/void/callback',
  response_type = 'code',
  scope = 'openid email offline_access',
  state = state,
})

resp = {}

url = 'https://auth.tesla.com/oauth2/v3/authorize?' .. args
res, code, headers = http.request({
  url = url,
  sink = ltn12.sink.table(resp),
  protocol = 'tlsv12',
})

if not res or code ~= 200 then
  log('request 1 failed', res, code)
  return
end

postdata = {}
regexp = '<input type="hidden" name="([^"]+)" value="([^"]*)"'

resp = table.concat(resp)
for name, value in resp:gmatch(regexp) do
  postdata[ name ] = value
end

postdata.identity = username
postdata.credential = password

cookie = headers['Set-Cookie'] or headers['set-cookie'] or ''
body = encodeargs(postdata)

res, code, headers = http.request({
  url = url,
  method = 'POST',
  source = ltn12.source.string(body),
  protocol = 'tlsv12',
  headers = {
    ['Content-Type'] = 'application/x-www-form-urlencoded',
    ['Content-Length'] = #body,
    ['Cookie'] = cookie,
  }
})

if not res or code ~= 302 then
  log('request 2 failed', res, code)
  return
end

hdr = headers.Location or headers.location
resp_code = hdr:match('code=([^&]+)')

body = json.encode({
  grant_type = 'authorization_code',
  client_id = 'ownerapi',
  code = resp_code,
  code_verifier = code_verifier,
  redirect_uri = 'https://auth.tesla.com/void/callback',
})

resp = {}

res, code, headers = http.request({
  url = 'https://auth.tesla.com/oauth2/v3/token',
  method = 'POST',
  source = ltn12.source.string(body),
  sink = ltn12.sink.table(resp),
  protocol = 'tlsv12',
  headers = {
    ['Content-Type'] = 'application/json',
    ['Accept'] = 'application/json',
    ['Content-Length'] = #body,
    ['User-Agent'] = 'Mozilla/5.0 (Linux; Android 9.0.0; VS985 4G Build/LRX21Y; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.83 Mobile Safari/537.36',
    ['X-Tesla-User-Agent'] = 'TeslaApp/3.4.4-350/fad4a582e/android/9.0.0',
  }
})

if not res or code ~= 200 then
  log('request 3 failed', res, code)
  return
end

resp = table.concat(resp)
resp = json.pdecode(resp)

bearer_token = resp.access_token
refresh_token = resp.refresh_token

body = json.encode({
  grant_type = 'urn:ietf:params:oauth:grant-type:jwt-bearer',
  client_id = client_id,
  client_secret = client_secret,
})

resp = {}

res, code, headers = http.request({
  url = 'https://owner-api.teslamotors.com/oauth/token',
  method = 'POST',
  source = ltn12.source.string(body),
  sink = ltn12.sink.table(resp),
  protocol = 'tlsv12',
  headers = {
    ['Content-Type'] = 'application/json',
    ['Authorization'] = 'Bearer ' .. bearer_token,
    ['Content-Length'] = #body,
  }
})

print(res, code)
if not res or code ~= 200 then
  log('request 4 failed', res, code)
  return
end

resp = table.concat(resp)
resp = json.pdecode(resp)

access_token = resp.access_token
log(access_token)
Reply
#10
Generating the new auth_token is working on my SHAC (with the old libraries)

Thank you!!!


 I'd like to use this token to automate a change in mode from 'backup-only' (which forces the Powerwall to charge from the Grid when electricity rates are cheap) to 'self-powered' (which discharges the battery to power our home).



Could you please also help me convert the python code to pass this token back to the Tesla Powerwall to change the mode to Backup 

https://github.com/fkhera/powerwallCloud...up.py#L132


Code:
## Set Powerwall Operation to Charge (Backup) or Discharge (self_consumption)
    #  Pause PERHAPS WITH (self_consumption) w/ Current SoC as backup_reserve_percent
    def operation_set(self, real_mode, backup_reserve_percent):
        # auth_header = {'Authorization': 'Bearer ' + self.token}
        payload = {"backup_reserve_percent": backup_reserve_percent}
        #payload = json.dumps({"real_mode": real_mode, "backup_reserve_percent": backup_reserve_percent})

        set_endpoint = '/backup'
        set_url = self.energy_base_url + str(self.energy_site_id) + set_endpoint
        print ("Setting Operation for Site Id: ", self.energy_site_id)
        print ("Trying URL: ", set_url)

        print ("Setting mode: " + json.dumps(payload))

        try:
            result = requests.post(set_url, json=payload, headers=self.auth_header, timeout=50)
            print("Set result output: ", result.content)
            if result.status_code == 201:
                print("Successfully changed reserve mode")
        except HTTPError as err:
            print("Error: {0}".format(err))
        except Timeout as err:
            print("Request timed out: {0}".format(err))#

Many thanks again

Kind Regards
James
Reply
#11
This should provide a list of products which id should be used in the next request. Change gateway URL and token as needed.
Code:
http = require('socket.http')
ltn12 = require('ltn12')
json = require('json')

access_token = 'ABCDEF'

url = 'https://192.168.1.1/api/1/products'

resp = {}
res, code, headers = http.request({
  url = url,
  method = 'GET',
  sink = ltn12.sink.table(resp),
  protocol = 'tlsv12',
  headers = {
    ['Authorization'] = 'Bearer ' .. access_token,
  }
})

resp = table.concat(resp)
log(res, code, resp)

This request will set the battery reserve percent to 50. energy_site_id is the ID from the previous request.
Code:
http = require('socket.http')
ltn12 = require('ltn12')
json = require('json')

access_token = 'ABCDEF'
energy_site_id = '123456'
backup_reserve_percent = 50

url = 'https://192.168.1.1/api/1/energy_sites/' .. energy_site_id .. '/backup'

body = json.encode({
  backup_reserve_percent = backup_reserve_percent,
})

res, code, headers = http.request({
  url = url,
  method = 'POST',
  source = ltn12.source.string(body),
  protocol = 'tlsv12',
  headers = {
    ['Content-Type'] = 'application/json',
    ['Content-Length'] = #body,
    ['Authorization'] = 'Bearer ' .. access_token,
  }
})

log(res, code, headers)
Reply
#12
I've implemented the above and can report back that it's working!

The IP address in the URL above should be replaced with  'https://owner-api.teslamotors.com'


I'm still having a little trouble capturing the energy_site_id .. how does one traverse the returned table with a numeric (1) table name.


https://owner-api.teslamotors.com/api/1/products  returns data in the form 

Code:
* table:
[response]
  * table:
  [1]
    * table:
    [site_name]
      * string: Home Energy Gateway
    [energy_site_id]
      * string: xxxxxxx
    [id]
      * string: xxxx-xxx-xxxx-xxxx


which I'd normally process as follows

Code:
mydata = json.pdecode(table.concat(resp))

assetSiteD = mydata.response.1.energy_site_id


however the table name '1' in the above errors when I try to save the script

Code:
Lua syntax error at line 200: malformed number near '.1.energy_site_id'


How do I reference the value with a table name of 1?


Kind Regards
James
Reply
#13
Try this, or put quotes around 1 if it does not work.
Code:
assetSiteD = mydata.response[1].energy_site_id
Reply
#14
The [1] works ... the quotes did not.

Many thanks
Reply
#15
yes it works.. great
Reply


Forum Jump: