Welcome to Tesla Motors Club
Discuss Tesla's Model S, Model 3, Model X, Model Y, Cybertruck, Roadster and More.
Register

PowerWall and switchgear API's

This site may earn commission on affiliate links.
I'm trying to log the data from my PowerWalls; the Tesla App is insufficient by leaps and bounds (no history past yesterday, not detailed enough, etc.).

On the local Tesla switch IP#, I did "webcopy -r 1.2.3.4" (where 1.2.3.4 is the IP# of the Tesla switch (the box the Ethernet cable is plugged into) on my LAN) and got a directory structure like this:

Code:
app.hexnumber20nibbles.js
app.hexnumber32nibbles.css
favicon-16x16.png
favicon-32x32.png
favicon.ico
index.html
robots.txt
src/assets/fonts/Gotham-Black.otf
src/assets/fonts/Gotham-BlackIta.otf
src/assets/fonts/Gotham-Bold.otf
src/assets/fonts/Gotham-BoldIta.otf
src/assets/fonts/Gotham-Book.otf
src/assets/fonts/Gotham-BookIta.otf
src/assets/fonts/Gotham-Light.otf
src/assets/fonts/Gotham-LightIta.otf
src/assets/fonts/Gotham-Medium.otf
src/assets/fonts/Gotham-MediumIta.otf
src/assets/fonts/Gotham-Thin.otf
src/assets/fonts/Gotham-ThinIta.otf
src/assets/fonts/Gotham-Ultra.otf
src/assets/fonts/Gotham-UltraIta.otf
src/assets/fonts/Gotham-XLight.otf
src/assets/fonts/Gotham-XLightIta.otf
vendor.hexnumber20nibbles.js

where I wrote "hexnumberxnibbles" was a hexadecimal number of x nibbles.

I then took app.hexnumber20nibbles.js and put it through beautifier at Best Online JavaScript Tool to View,Beautify,Formatter,Minify,Obfuscator Javascript Code and got a file I can look at in Emacs. On line 2,066 of that file is the following:

Code:
r.api.uri = "/api", r.static.uri = "", t.default = r

Later in the file are things like api.uri such as default.api.uri. Here are some:

Code:
$ egrep api.uri app.xxx-beautified.js
  r.api.uri = "/api", r.static.uri = "", t.default = r
    return e(I()), fetch(k.default.api.uri + "/site_info").then(D.checkStatus).then(D.parseJSON).then(function(t) {
    return fetch(k.default.api.uri + "/site_info/site_name", {
    return e(B()), fetch(k.default.api.uri + "/operation", {
     return t && (i.mode = t), fetch(k.default.api.uri + "/operation", {
    return e(U()), e((0, S.clearError)(R.RECEIVE_SITEMASTER_SETTINGS_ERROR)), fetch(k.default.api.uri + "/sitemaster").then(D.checkStatus).then(D.parseJSON).then(function(t) {
    return e(V()), e((0, S.clearError)(R.RECEIVE_START_SITEMASTER_ERROR)), e((0, S.clearError)(R.RECEIVE_STOP_SITEMASTER_ERROR)), fetch(k.default.api.uri + "/sitemaster", {
    return e(K()), e((0, S.clearError)(R.RECEIVE_START_SITEMASTER_ERROR)), e((0, S.clearError)(R.RECEIVE_STOP_SITEMASTER_ERROR)), fetch(k.default.api.uri + "/sitemaster", {
    return t($()), fetch(k.default.api.uri + "/site_info/timezone", {
        return c(L()), n.prev = 1, n.next = 4, (0, A.timeoutPromise)(A.REQUEST_TIMEOUT, new Error("Request timed out"), fetch(b.default.api.uri + "/login/Basic", {
    return t(T()), fetch(b.default.api.uri + "/logout", {
    return e(Z()), e((0, P.clearError)(B.RECEIVE_DETECT_METER_ERROR)), fetch(U.default.api.uri + "/meters/detect_wired_meters", {
    return n(ee()), fetch(U.default.api.uri + "/meters/" + t, {
    return o(ne()), o((0, P.clearError)(B.RECEIVE_CREATE_METER_ERROR)), o((0, P.clearError)(B.RECEIVE_COMMISSION_METER_ERROR)), fetch(U.default.api.uri + "/meters", {
        return a(oe()), a((0, P.clearError)(B.RECEIVE_COMMISSION_METER_ERROR)), r.abrupt("return", fetch(U.default.api.uri + "/meters/" + n + "/commission", {
    return t(ie()), fetch(U.default.api.uri + "/meters", {
     return fetch(U.default.api.uri + "/meters/" + o.serial + "/cts", {
    return t(le()), fetch(U.default.api.uri + "/meters/" + e + "/cts", {
    return e(fe()), fetch(U.default.api.uri + "/meters/readings", {
    return n(Me()), fetch(U.default.api.uri + "/meters/" + e + "/invert_cts", {
    return e(he()), fetch(U.default.api.uri + "/meters/aggregates").then(F.checkStatus).then(F.parseText).then(F.parseResponseText).then(F.checkResponseStatus).then(function(t) {
        return e.abrupt("return", fetch(U.default.api.uri + "/meters/" + n + "/verify", {
        return e.abrupt("return", fetch(U.default.api.uri + "/meters/status", {
    return e(y()), e((0, M.clearError)(h.RECEIVE_CONFIG_INITIALIZED_ERROR)), fetch(b.default.api.uri + "/status").then(A.parseText).then(A.parseResponseText).then(A.checkResponseStatus).then(function(t) {
        return n(v()), n((0, M.clearError)(h.RECEIVE_SYNC_CONFIG_ERROR)), e.prev = 2, e.next = 5, (0, A.timeoutPromise)(A.REQUEST_TIMEOUT, new Error("Config sync timed out"), fetch(b.default.api.uri + "/config/completed", {
       return e.abrupt("return", fetch(b.default.api.uri + "/config", {
    return e(re()), fetch(J.default.api.uri + "/networks", {
    return e(oe()), fetch(J.default.api.uri + "/system/networks", {
    return e(ie()), fetch(J.default.api.uri + "/networks/wifi_security_types", {
    return e(ce()), fetch(J.default.api.uri + "/networks/wifi_scan", {
    return e(ue()), fetch(J.default.api.uri + "/networks/wifi_scan", {
    return n && (0, ee.requiresPassword)(e) && (a.password = n), r && (0, ee.requiresUsername)(e) && (a.username = r), fetch(J.default.api.uri + "/networks", {
    return r(Me()), fetch(J.default.api.uri + "/networks/" + e, {
    return t(_e()), fetch(J.default.api.uri + "/networks/" + e, {
    return fetch(J.default.api.uri + "/networks/" + e + "/disable", {
        return te = 0, e.abrupt("return", fetch(J.default.api.uri + "/networks/" + t + "/enable", {
        return e.abrupt("return", fetch(J.default.api.uri + "/system/networks/ping_test", {
        return e.abrupt("return", fetch(J.default.api.uri + "/system/networks", {
    return e(m()), e((0, l.clearError)(f.RECEIVE_POWERWALLS_ERROR)), (0, u.default)(M.default.api.uri + "/powerwalls", {
    return (0, u.default)(M.default.api.uri + "/powerwalls", {
    return e(T()), fetch(y.default.api.uri + "/solar", {
    return e(g()), fetch(y.default.api.uri + "/solar/brands", {
    return t(O()), fetch(y.default.api.uri + "/solar/brands/" + e, {
     return e(S()), fetch(y.default.api.uri + "/solar", {
    return e(L()), fetch(E.default.api.uri + "/site_info", {
    return e(v()), fetch(E.default.api.uri + "/site_info/grid_codes", {
    return n(T()), fetch(E.default.api.uri + "/site_info/grid_code", {
    return e(T()), fetch(y.default.api.uri + "/customer", {
    return fetch(y.default.api.uri + "/customer", {
    return e(S()), fetch(y.default.api.uri + "/customer/registration/emailed", {
    return e(R()), fetch(y.default.api.uri + "/customer/registration/skip", {
  r.api.uri = "/api", r.static.uri = "", t.default = r
    return e(I()), fetch(k.default.api.uri + "/site_info").then(D.checkStatus).then(D.parseJSON).then(function(t) {
    return fetch(k.default.api.uri + "/site_info/site_name", {
    return e(B()), fetch(k.default.api.uri + "/operation", {
     return t && (i.mode = t), fetch(k.default.api.uri + "/operation", {
    return e(U()), e((0, S.clearError)(R.RECEIVE_SITEMASTER_SETTINGS_ERROR)), fetch(k.default.api.uri + "/sitemaster").then(D.checkStatus).then(D.parseJSON).then(function(t) {
    return e(V()), e((0, S.clearError)(R.RECEIVE_START_SITEMASTER_ERROR)), e((0, S.clearError)(R.RECEIVE_STOP_SITEMASTER_ERROR)), fetch(k.default.api.uri + "/sitemaster", {
    return e(K()), e((0, S.clearError)(R.RECEIVE_START_SITEMASTER_ERROR)), e((0, S.clearError)(R.RECEIVE_STOP_SITEMASTER_ERROR)), fetch(k.default.api.uri + "/sitemaster", {
    return t($()), fetch(k.default.api.uri + "/site_info/timezone", {
        return c(L()), n.prev = 1, n.next = 4, (0, A.timeoutPromise)(A.REQUEST_TIMEOUT, new Error("Request timed out"), fetch(b.default.api.uri + "/login/Basic", {
    return t(T()), fetch(b.default.api.uri + "/logout", {
    return e(Z()), e((0, P.clearError)(B.RECEIVE_DETECT_METER_ERROR)), fetch(U.default.api.uri + "/meters/detect_wired_meters", {
    return n(ee()), fetch(U.default.api.uri + "/meters/" + t, {
    return o(ne()), o((0, P.clearError)(B.RECEIVE_CREATE_METER_ERROR)), o((0, P.clearError)(B.RECEIVE_COMMISSION_METER_ERROR)), fetch(U.default.api.uri + "/meters", {
        return a(oe()), a((0, P.clearError)(B.RECEIVE_COMMISSION_METER_ERROR)), r.abrupt("return", fetch(U.default.api.uri + "/meters/" + n + "/commission", {
    return t(ie()), fetch(U.default.api.uri + "/meters", {
     return fetch(U.default.api.uri + "/meters/" + o.serial + "/cts", {
    return t(le()), fetch(U.default.api.uri + "/meters/" + e + "/cts", {
    return e(fe()), fetch(U.default.api.uri + "/meters/readings", {
    return n(Me()), fetch(U.default.api.uri + "/meters/" + e + "/invert_cts", {
    return e(he()), fetch(U.default.api.uri + "/meters/aggregates").then(F.checkStatus).then(F.parseText).then(F.parseResponseText).then(F.checkResponseStatus).then(function(t) {
        return e.abrupt("return", fetch(U.default.api.uri + "/meters/" + n + "/verify", {
        return e.abrupt("return", fetch(U.default.api.uri + "/meters/status", {
    return e(y()), e((0, M.clearError)(h.RECEIVE_CONFIG_INITIALIZED_ERROR)), fetch(b.default.api.uri + "/status").then(A.parseText).then(A.parseResponseText).then(A.checkResponseStatus).then(function(t) {
        return n(v()), n((0, M.clearError)(h.RECEIVE_SYNC_CONFIG_ERROR)), e.prev = 2, e.next = 5, (0, A.timeoutPromise)(A.REQUEST_TIMEOUT, new Error("Config sync timed out"), fetch(b.default.api.uri + "/config/completed", {
       return e.abrupt("return", fetch(b.default.api.uri + "/config", {
    return e(re()), fetch(J.default.api.uri + "/networks", {
    return e(oe()), fetch(J.default.api.uri + "/system/networks", {
    return e(ie()), fetch(J.default.api.uri + "/networks/wifi_security_types", {
    return e(ce()), fetch(J.default.api.uri + "/networks/wifi_scan", {
    return e(ue()), fetch(J.default.api.uri + "/networks/wifi_scan", {
    return n && (0, ee.requiresPassword)(e) && (a.password = n), r && (0, ee.requiresUsername)(e) && (a.username = r), fetch(J.default.api.uri + "/networks", {
    return r(Me()), fetch(J.default.api.uri + "/networks/" + e, {
    return t(_e()), fetch(J.default.api.uri + "/networks/" + e, {
    return fetch(J.default.api.uri + "/networks/" + e + "/disable", {
        return te = 0, e.abrupt("return", fetch(J.default.api.uri + "/networks/" + t + "/enable", {
        return e.abrupt("return", fetch(J.default.api.uri + "/system/networks/ping_test", {
        return e.abrupt("return", fetch(J.default.api.uri + "/system/networks", {
    return e(m()), e((0, l.clearError)(f.RECEIVE_POWERWALLS_ERROR)), (0, u.default)(M.default.api.uri + "/powerwalls", {
    return (0, u.default)(M.default.api.uri + "/powerwalls", {
    return e(T()), fetch(y.default.api.uri + "/solar", {
    return e(g()), fetch(y.default.api.uri + "/solar/brands", {
    return t(O()), fetch(y.default.api.uri + "/solar/brands/" + e, {
     return e(S()), fetch(y.default.api.uri + "/solar", {
    return e(L()), fetch(E.default.api.uri + "/site_info", {
    return e(v()), fetch(E.default.api.uri + "/site_info/grid_codes", {
    return n(T()), fetch(E.default.api.uri + "/site_info/grid_code", {
    return e(T()), fetch(y.default.api.uri + "/customer", {
    return fetch(y.default.api.uri + "/customer", {
    return e(S()), fetch(y.default.api.uri + "/customer/registration/emailed", {
    return e(R()), fetch(y.default.api.uri + "/customer/registration/skip", {

I went to the URL http://1.2.3.4/api/site_info and got this:

Code:
site_name: "Home Energy Gateway"
timezone: "America/Los_Angeles"
min_site_meter_power_kW: -1000000000
max_site_meter_power_kW: 1000000000
nominal_system_energy_kWh: 13.5
grid_code: "60Hz_240V_s_IEEE1547:2003"
grid_voltage_setting: 240
grid_freq_setting: 60
grid_phase_setting: "Split"
country: "United States"

Note that it seems to want to talk to the Smart Meter a bit in the code, or seems to. Note that IEEE1547:2003 is IEEE 1547 which is "IEEE 1547 Standard for Interconnecting Distributed Resources with Electric Power Systems"; I wonder what interconnection information we could get that way? Anyway, here's some of the other api responses:

http://1.2.3.4/api/operation returns:
Code:
code 403
error "Unable to GET to resource"
message "User does not have adequate access rights"
What access rights do I need? Is there a key?

Just now, I realized that there are three tabs in Firefox: JSON, Raw Data, and Headers. Firefox automatically selected JSON. For instance, for api/sitemaster, I got in JSON:

Code:
running true
uptime "7706s,"
connected_to_tesla true

In Raw Data, I got:
Code:
{"running":true,"uptime":"7706s,","connected_to_tesla":true}

I think I like the Raw Data format better programmatically, but for simplicity of understanding, I think I'll stick to JSON, unless the response is not very useful, in which case, I'll just write the Raw Data.

Headers gives me:
Code:
Access-Control-Allow-Methods GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS
Access-Control-Allow-Origin *
Access-Control-Max-Age 86400
Content-Encoding gzip
Content-Length 80
Content-Type application/json
Date Thu, 27 Jul 2017 07:36:54 GMT
access-control-allow-credentials false
access-control-allow-headers X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept, Accept-Encoding, Authorization

Those headers sure tell us a lot about what kind of authorizations it wants.

I'm going to keep showing some more URI outputs:
api/meters:
Code:
{"code":403,"error":"Unable to GET to resource","message":"User does not have adequate access rights"}

I get the same response for api/networks and api/system/networks.

Here's api/powerwalls:

Code:
{"powerwalls":[{"PackagePartNumber":"1092170-03-E","PackageSerialNumber":"T17E0004XXX"},{"PackagePartNumber":"1092170-03-E","PackageSerialNumber":"T17F0004XXX"}],"hasSync":true}
In JSON:
Code:
powerwalls:
  0:
    PackagePartNumber "1092170-03-E"
    PackageSerialNumber "T17E0004XXX"
  1:
    PackagePartNumber: "1092170-03-E"
    PackageSerialNumber: "T17F0004XXX"
  hasSync: true
XXX were numbers corresponding to my equipment serial numbers.

api/solar was unauthorized.

I'm going to keep looking at this Javascript until I find out where it gets the readings from in the display that I see when I go to its home page.
 
Here's some interesting code for login:
Code:
  function i(e, t) {
   var n = this,
    r = arguments.length > 2 && void 0 !== arguments[2] && arguments[2],
    o = arguments[3];
   return function() {
    var i = (0, p.default)(u.default.mark(function i(c) {
     return u.default.wrap(function(n) {
      for (;;) switch (n.prev = n.next) {
       case 0:
        return c(L()), n.prev = 1, n.next = 4, (0, A.timeoutPromise)(A.REQUEST_TIMEOUT, new Error("Request timed out"), fetch(b.default.api.uri + "/login/Basic", {
         method: "POST",
         credentials: "same-origin",
         headers: {
          "Content-Type": "application/json"
         },
         body: (0, d.default)({
          username: e,
          password: t,
          force_sm_off: r
         })
        }).then(A.parseText).then(A.parseResponseText).then(A.checkResponseStatus).then(function(e) {
         c(a(e)), o && o(e)
        }));
       case 4:
        n.next = 10;
        break;
       case 6:
        n.prev = 6, n.t0 = n.catch(1), c((0, M.showError)(h.RECEIVE_LOGIN_ERROR, n.t0.toString(), n.t0.response)), c(v());
       case 10:
       case "end":
        return n.stop()
      }
     }, i, n, [
      [1, 6]
     ])
    }));
    return function(e) {
     return i.apply(this, arguments)
    }
   }()
  }

After skimming Tesla Model S REST API, I realized that web apps send data in RFC733/RFC822/RFC977 type headers, which I remember also HTTP uses. Obviously, the case 0: is the headers, and the body: is the body. In the body, it shows username: e, and password: t. It is function i(e,t), so I wonder where the caller gets e,t from.

I don't know JAVA, so I'm a little baffled.

In another thread, someone said you can provision this thing from the login ("RUN WIZARD" on home page), so that explains a lot of the provisioning stuff in the code. At this time, I have no interest in provisioning it; it was provisioned by the authorized installer.
 
  • Informative
Reactions: brianman
Found it! Thanks to wireshark and wireshark-http-gunzip.

Code:
GET /api/meters/aggregates HTTP/1.1
Host: 10.1.0.15
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:54.0) Gecko/20100101 Firefox/54.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
origin: http://10.1.0.15
Referer: http://10.1.0.15/
Connection: keep-alive

HTTP/1.1 200 OK
Access-Control-Allow-Credentials: false
Access-Control-Allow-Credentials: false
Access-Control-Allow-Headers: X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept, Accept-Encoding, Authorization
Access-Control-Allow-Headers: X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept, Accept-Encoding, Authorization
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS
Access-Control-Allow-Origin: *
Access-Control-Allow-Origin: *
Access-Control-Max-Age: 86400
Access-Control-Max-Age: 86400
Content-Type: application/json
Date: Thu, 27 Jul 2017 09:19:59 GMT
Transfer-Encoding: chunked

842
{"site":{"last_communication_time":"2017-07-27T09:19:58.969380696Z","instant_power":-9.509994506835938,"instant_reactive_power":3.44000244140625,"instant_apparent_power":10.113041694610512,"frequency":60,"energy_exported":49924.15972227374,"energy_imported":18247.312222273744,"instant_average_voltage":239.76000213623047,"instant_total_current":0,"i_a_current":0,"i_b_current":0,"i_c_current":0},"battery":{"last_communication_time":"2017-07-27T09:19:58.95119964Z","instant_power":500,"instant_reactive_power":210,"instant_apparent_power":542.3098745182499,"frequency":59.938,"energy_exported":86520,"energy_imported":114950,"instant_average_voltage":239.75,"instant_total_current":-13.4,"i_a_current":0,"i_b_current":0,"i_c_current":0},"load":{"last_communication_time":"2017-07-27T09:19:58.95119964Z","instant_power":478.8129181478954,"instant_reactive_power":220.16620066112372,"instant_apparent_power":527.0056607844524,"frequency":60,"energy_exported":0,"energy_imported":149800.18611111114,"instant_average_voltage":239.76000213623047,"instant_total_current":1.9970508586992597,"i_a_current":0,"i_b_current":0,"i_c_current":0},"solar":{"last_communication_time":"2017-07-27T09:19:58.969459696Z","instant_power":-3.6699999570846558,"instant_reactive_power":18.859999656677246,"instant_apparent_power":19.213757746335492,"frequency":60,"energy_exported":210201.27472223042,"energy_imported":294.24111111929506,"instant_average_voltage":239.76000213623047,"instant_total_current":0,"i_a_current":0,"i_b_current":0,"i_c_current":0},"busway":{"last_communication_time":"0001-01-01T00:00:00Z","instant_power":0,"instant_reactive_power":0,"instant_apparent_power":0,"frequency":0,"energy_exported":0,"energy_imported":0,"instant_average_voltage":0,"instant_total_current":0,"i_a_current":0,"i_b_current":0,"i_c_current":0},"frequency":{"last_communication_time":"0001-01-01T00:00:00Z","instant_power":0,"instant_reactive_power":0,"instant_apparent_power":0,"frequency":0,"energy_exported":0,"energy_imported":0,"instant_average_voltage":0,"instant_total_current":0,"i_a_current":0,"i_b_current":0,"i_c_current":0}}
0

Now I know why so much of the Javascript seemed to be so focused on unit conversion.

And, apparently, I don't know how to use Wireshark, still. Ugh! Annoying ... I just want the complete sequential stream, uncompressed. No simple way?
 
It gets:
  1. /
  2. /app.hex.css
  3. /app.hex.js
  4. /vender.hex.js
  5. /favicon.ico
  6. /0.network.hex.js
  7. /hex.png
  8. /api/status
  9. fonts
Then it loops on these three:
  1. /api/sitemaster
  2. /api/site_info
  3. /api/meters/aggregates
I think I'll do the same thing every second and see what happens.

Looks like the command is easy in curl:

curl -s 'http://1.2.3.4/api/{sitemaster,site_info,meters/aggregates}'
 
Last edited:
My notes on return values for http://w.x.y.z/api/meters/aggregate :

Meters:
  • solar=Solar Panels Inverter connection
  • battery=PowerWall connection
  • load=House subpanel connection
  • site=PG&E Grid connection (this is the least sensible name of the list)
  • busway: empty (probably relevant for other setups; for instance, perhaps for full house?)
  • frequency: empty (is this for some type of frequency monitor that I don't have or need?)
Units:

Let's start with an example:
Code:
'load':
  'last_communication_time': '2017-07-28T02:18:44.451244454Z'
  'instant_power': 1520.8155457067037
  'instant_reactive_power': -251.61099383215097
  'instant_apparent_power': 1541.4888959315865
  'frequency': 60
  'energy_exported': 0
  'energy_imported': 168136.1375
  'instant_average_voltage': 239.86000061035156
  'instant_total_current': 6.340430008491672
  'i_a_current': 0
  'i_b_current': 0
  'i_c_current': 0
Units:
  • last_communication_time: ISO date YYYY-MM-DDThh:mm:ss.sssssssssZ (Z is Zulu for timezone Zero). January 1st of the Year 0001 at 00:00:00 Zulu is no data.
  • instant_power: watt (W)
  • instant_reactive_power: volt ampere reactive (var)
  • instant_apprent_power: volt ampere (VA)
  • frequency: hertz (Hz)
  • energy_exported: watt hour (Wh)
  • energy_imported: watt hour (Wh)
  • instant_average_voltage: instantaneous voltage (V inst)
  • instant_total_current: amp (A)
I set up a MYSQL database, and am going to store the values per meter with the above field names and field values.
 
It occurs to me I ought to use a JSON database (e.g., MongoDB); that way, whenever Tesla PowerWall changes its output, only my graphing apps break, not the database collection stuff, unless the APIs change. However, I don't know Java, and I don't know MongoDB. I really can't help anyone else be smart at this point; the pieces are there for others to do better than me at this point.
 
I'm missing key information: I'm missing the battery level, as well as the ability to adjust its reserve setting.

I just found this, and want more insight into how to authenticate (login?):
Code:
  function m(e) {
   var t = e.mode,
    n = e.backupReserve,
    r = e.generationLimit,
    o = e.solarLimit,
    a = e.batteryLimit;
   return "string" == typeof r && (r = parseFloat(r)),
    "string" == typeof o && (o = parseFloat(o)), "string" == typeof a && (a = parseFloat(a)),
    function(e) {
     e(j()), e((0, S.clearError)(R.RECEIVE_SAVE_OPERATION_SETTINGS_ERROR));
     var i = {
      backup_reserve_percent: n,
      max_pv_export_power_kW: o
     };
     return t && (i.mode = t), fetch(k.default.api.uri + "/operation", {
      method: "POST",
      credentials: "same-origin",
      headers: {
       "Content-Type": "application/json"
      },
      body: (0, T.default)(i)
     }).then(D.parseText).then(D.parseResponseText).then(D.checkResponseStatus).then(function(i) {
      e(s({
       mode: t,
       backupReserve: n,
       generationLimit: r,
       solarLimit: o,
       batteryLimit: a
      }))
     }).catch(function(t) {
      e((0, S.showError)(R.RECEIVE_SAVE_OPERATION_SETTINGS_ERROR, t.toString(), t.response)), e(H())
     })
    }
 
I just found this, and want more insight into how to authenticate (login?):

A couple of notes:
1. The javascript you found (Javascript is, btw, *not* the same as Java) may not be relevant to the app - If Tesla developed a native app, the code that's running would be in the app itself. The javascript you found is used for a web interface into the Powerwall.
2. In order to determine how to login, what you need to do is capture the traffic generated when the app connects to the Powerwall. Note that the app may be able to reuse sessions, so you may not see this every time, but I'm guessing if you exit the app and launch it again enough times, you should see a login.
3. In order to determine how to set the level, perform this operation while your capture is running.
 
  • Helpful
Reactions: Ulmo
A couple of notes:
1. The javascript you found (Javascript is, btw, *not* the same as Java) may not be relevant to the app - If Tesla developed a native app, the code that's running would be in the app itself. The javascript you found is used for a web interface into the Powerwall.
2. In order to determine how to login, what you need to do is capture the traffic generated when the app connects to the Powerwall. Note that the app may be able to reuse sessions, so you may not see this every time, but I'm guessing if you exit the app and launch it again enough times, you should see a login.
3. In order to determine how to set the level, perform this operation while your capture is running.

Regarding #2-#3, I'm going to have to get the sniffer into position. I have to read about asking switches to send me sniff data from 1-2 switch hops away (I don't currently have a wire long enough to reposition my sniffer in such a way, and the danger of moving it would be excessive). My assumption was that controlling my PowerWall was a private affair, and it would be encrypted, so that's why I didn't yet try that.

Ahh, the days of youth, when everything was an arm's reach away, and we longed for the days of age when we could stretch our legs out. I don't miss them. I like this problem :D

Regarding #1, I know, but since I'm on a hunt, I'm willing to look at any clue, relevant or not.

I think knowing the protocol the Mothership (Tesla HQ) communicates to the PowerWall would be good to know. I look forward to figuring out how to capture this.
 
Last edited:
So, recap of what I've gotten so far:
  • No state of charge data; probably need deeper hacking, sniffing, etc.
  • No control interface (would be derivative of above)
  • Manual polling of flow data is working; I save it however I want; right now into MySQL.
    • Graphing it: my learning curve on Splunk is really annoying me (I blame the point and click interface). Luckily, a quick search brings up many, many alternatives. So far with Splunk:
      • The wrong item showed up as the X-axis; I'm trying to select the X axis. No way to do it.
      • Having trouble selecting what fields to graph.
      • Generally very frustrating.
      • Probably going to try another graphing software.
 
I started to do something similar, but didn't know about beautifiers, and mostly just searched through the JS. Great work so far!

I did run into the auth stuff as well and wondered if supplying the installer credentials might help with that. But it sounds like doing the setup workflow immediately decommissions the system, which is something I don't want to do yet. SOC data would be very handy.

I also kind of wonder if there's a way to get the firmware off the gateway, like if it's stored on an SD card, or USB drive, or similar. Knowing my luck, though, it's probably a NAND part soldered to the board.
 
Regarding #2-#3, I'm going to have to get the sniffer into position. I have to read about asking switches to send me sniff data from 1-2 switch hops away (I don't currently have a wire long enough to reposition my sniffer in such a way, and the danger of moving it would be excessive). My assumption was that controlling my PowerWall was a private affair, and it would be encrypted, so that's why I didn't yet try that.[...]

Sorry - I didn't read your previous messages carefully enough - I thought you were already sniffing traffic between the app and the Powerwall. I was quite surprised that this was going over an unencrypted connection, but figured since it's on your LAN, Tesla may have thought that the protection would come from your LAN's security.

You're probably right that the app traffic would be unencrypted.

You're not going to be able to sniff 3rd party traffic on a switch, typically.

The best way to sniff is probably to sniff the wireless traffic - see, for example: ::wolfhoundsec::: Monitoring WPA2 wireless traffic w/ Wireshark

If the connection is using https: you're going to be kind of screwed - sniffing will be much more complicated. Here's a possible starting point: How to debug HTTP(s) traffic on Android – Sebastiano Gottardo – Medium
 
At least I have my first graph of power flows, if not state of charge of batteries:
power.png


Done via curl http://x/api/meters/aggregates, jsmn, bash, mysql, bash, gnuplot.
 
Last edited:
I started to do something similar, but didn't know about beautifiers, and mostly just searched through the JS. Great work so far!

I did run into the auth stuff as well and wondered if supplying the installer credentials might help with that. But it sounds like doing the setup workflow immediately decommissions the system, which is something I don't want to do yet. SOC data would be very handy.

I also kind of wonder if there's a way to get the firmware off the gateway, like if it's stored on an SD card, or USB drive, or similar. Knowing my luck, though, it's probably a NAND part soldered to the board.
I was able to figure out the Tesla mothership app server API for getting the level of charge. Here's my post about it:

Model S REST API

They show up as orange dots on my graphs that I'm currently sharing with the public while we all figure this stuff out: Ulmo.Solar I was inspired by someone else to follow their website naming convention; it's purely from respect, not plagiarism, but I do intentionally copy the best.

I still want to know how to authorize to get more data (state of charge), and control my PowerWalls, from both places (LAN and the mothership).
 
  • Like
Reactions: Drewflux
Regarding the "User does not have adequate access rights," I looked at the source code for the landing page on the provisioning wizard. It references two js files one of which (on my system anyway) is at /app.94070648a056d37c80dc.js
This one references:
fetch(k.default.api.uri+"/operation",{method:"POST",credentials:"same-origin"

I believe this means it is using a cookie based login. You might be able to get the cookie set in your local browser by logging into the wizard and commissioning your system. I assume you would though have to create a specially formed URL request to make sure that credentials:"same-origin" is passed. Remember the login to the wizard is any e-mail address, password is the gateway serial number in all caps.

You might also be able to just borrow this js file and use it for authentication since it looks like this script also handles logging in via
/login/Basic and setting "AuthCookie". Then you can use Chrome's developer functions to examine the auth cookie. When I did this, it just set a seemingly random login string as the AuthCookie value, expiry is "session."
 
  • Informative
Reactions: Ulmo