wip(tesla_api_coverage): scrape vehicle command
This commit is contained in:
parent
503effc7ef
commit
d0b8f6df67
16
API.md
Normal file
16
API.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
# Tesla API Matrix
|
||||
|
||||
List of all known Tesla APIs, and if this crate supports it, and which of the Tesla enpoints support them.
|
||||
|
||||
### Legend
|
||||
|
||||
- Blank - Unknown
|
||||
- ✅ Supported by this crate
|
||||
- 🟢 Supported by specified API
|
||||
- 🔴 Not supported by specified API
|
||||
|
||||
Currently only the Owner API is partially supported by this crate.
|
||||
|
||||
| API | Owner API | Fleet API | Command Mode |
|
||||
|-----------| --- | --- | --- |
|
||||
| honk_horn | ✅ | | |
|
1755
scrape_fleet_api/Cargo.lock
generated
1755
scrape_fleet_api/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,425 +0,0 @@
|
|||
use clap::{Command, Parser};
|
||||
use scraper::{Element, ElementRef, Html, Selector};
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(author, version)]
|
||||
struct Cli {
|
||||
/// Only use the cached html to avoid making a request.
|
||||
#[clap(short, long)]
|
||||
cache: bool,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
let args = Cli::parse();
|
||||
|
||||
let html = get_and_save_html(&args).await;
|
||||
|
||||
let fleet_api = parse(&html);
|
||||
}
|
||||
|
||||
async fn get_and_save_html(args: &Cli) -> String {
|
||||
// Write to where this project root is, not in the parent project.
|
||||
let mut path = PathBuf::new();
|
||||
path.push(env!("CARGO_MANIFEST_DIR"));
|
||||
path.push("fleet.html");
|
||||
|
||||
if args.cache {
|
||||
return std::fs::read_to_string(path).unwrap();
|
||||
}
|
||||
|
||||
let response = reqwest::get("https://developer.tesla.com/docs/fleet-api")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let html = response.text().await.unwrap();
|
||||
|
||||
std::fs::write(path, &html).unwrap();
|
||||
|
||||
html
|
||||
}
|
||||
|
||||
struct FleetApiSpec {
|
||||
calls: HashMap<String, Call>,
|
||||
}
|
||||
|
||||
// e.g. serialize to similar: vehicle-endpoints
|
||||
#[derive(Debug, strum::EnumString)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
enum Category {
|
||||
ChargingEndpoints,
|
||||
PartnerEndpoints,
|
||||
UserEndpoints,
|
||||
VehicleCommands,
|
||||
VehicleEndpoints,
|
||||
}
|
||||
|
||||
/*
|
||||
Profile Information user_data Contact information, home address, profile picture, and referral information
|
||||
Vehicle Information vehicle_device_data Vehicle live data, location, eligible upgrades, nearby superchargers, ownership, and service scheduling data
|
||||
Vehicle Commands vehicle_cmds Commands like add/remove driver, access Live Camera, unlock, wake up, remote start, and schedule software updates
|
||||
Vehicle Charging Management vehicle_charging_cmds Vehicle charging history, billed amount, charging location, commands to schedule, and start/stop charging
|
||||
Energy Product Information energy_device_data Energy flow history, saving forecast, tariff rates, grid import, calendar, site status, time of use, and ownership
|
||||
Energy Product Commands energy_cmds Commands like update storm mode
|
||||
*/
|
||||
|
||||
#[derive(Debug, strum::EnumString)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
enum Scope {
|
||||
/// Profile Information
|
||||
///
|
||||
/// Contact information, home address, profile picture, and referral information.
|
||||
UserData,
|
||||
|
||||
/// Vehicle Information
|
||||
///
|
||||
/// Vehicle live data, location, eligible upgrades, nearby superchargers, ownership, and service scheduling data.
|
||||
VehicleDeviceData,
|
||||
|
||||
/// Vehicle Commands
|
||||
///
|
||||
/// Commands like add/remove driver, access Live Camera, unlock, wake up, remote start, and schedule software updates.
|
||||
VehicleCmds,
|
||||
|
||||
/// Vehicle Charging Management
|
||||
///
|
||||
/// Vehicle charging history, billed amount, charging location, commands to schedule, and start/stop charging.
|
||||
VehicleChargingCmds,
|
||||
|
||||
/// Energy Product Information
|
||||
///
|
||||
/// Energy flow history, saving forecast, tariff rates, grid import, calendar, site status, time of use, and ownership.
|
||||
EnergyDeviceData,
|
||||
|
||||
/// Energy Product Commands
|
||||
///
|
||||
/// Commands like update storm mode.
|
||||
EnergyCmds,
|
||||
}
|
||||
|
||||
/*
|
||||
Name In Type Required Description
|
||||
vin query string No VIN
|
||||
startTime query string No StartTime
|
||||
endTime query string No EndTime
|
||||
*/
|
||||
|
||||
enum InRequestData {
|
||||
Query,
|
||||
Body,
|
||||
}
|
||||
|
||||
struct Parameter {
|
||||
name: String,
|
||||
request: InRequestData,
|
||||
var_type: String,
|
||||
required: bool,
|
||||
description: String,
|
||||
}
|
||||
|
||||
struct Call {
|
||||
name: String,
|
||||
method: reqwest::Method,
|
||||
url_definition: String,
|
||||
description: String,
|
||||
category: Category,
|
||||
scopes: Vec<Scope>,
|
||||
parameters: Vec<Parameter>,
|
||||
request_example: String,
|
||||
response_example: String,
|
||||
}
|
||||
|
||||
/*
|
||||
Example HTML docs for two calls:
|
||||
|
||||
|
||||
<h1 id='vehicle-commands'>Vehicle Commands</h1><h2 id='actuate_trunk'>actuate_trunk</h2>
|
||||
<p><span class="endpoint"><code>POST /api/1/vehicles/{id}/command/actuate_trunk</code></span></p>
|
||||
|
||||
<p>scopes: <em>vehicle_cmds</em></p>
|
||||
<div class="highlight"><pre class="highlight shell tab-shell"><code>curl <span class="nt">--header</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
|
||||
<span class="nt">--header</span> <span class="s2">"Authorization: Bearer </span><span class="nv">$TESLA_API_TOKEN</span><span class="s2">"</span> <span class="se">\</span>
|
||||
<span class="nt">--data</span> <span class="s1">'{"which_trunk":"string"}'</span> <span class="se">\</span>
|
||||
<span class="s1">'https://fleet-api.prd.na.vn.cloud.tesla.com/api/1/vehicles/{id}/command/actuate_trunk'</span>
|
||||
</code></pre></div><div class="highlight"><pre class="highlight javascript tab-javascript"><code><span class="kd">const</span> <span class="nx">myHeaders</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Headers</span><span class="p">();</span>
|
||||
<span class="nx">myHeaders</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">);</span>
|
||||
<span class="nx">myHeaders</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="dl">"</span><span class="s2">Authorization</span><span class="dl">"</span><span class="p">,</span> <span class="s2">`Bearer </span><span class="p">${</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">TESLA_API_TOKEN</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
|
||||
|
||||
<span class="kd">const</span> <span class="nx">body</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span>
|
||||
<span class="dl">"</span><span class="s2">which_trunk</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span>
|
||||
<span class="p">});</span>
|
||||
|
||||
<span class="kd">const</span> <span class="nx">requestOptions</span> <span class="o">=</span> <span class="p">{</span>
|
||||
<span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
|
||||
<span class="na">headers</span><span class="p">:</span> <span class="nx">myHeaders</span><span class="p">,</span>
|
||||
<span class="nx">body</span><span class="p">,</span>
|
||||
<span class="na">redirect</span><span class="p">:</span> <span class="dl">'</span><span class="s1">follow</span><span class="dl">'</span>
|
||||
<span class="p">};</span>
|
||||
|
||||
<span class="nx">fetch</span><span class="p">(</span><span class="dl">"</span><span class="s2">https://fleet-api.prd.na.vn.cloud.tesla.com/api/1/vehicles/{id}/command/actuate_trunk</span><span class="dl">"</span><span class="p">,</span> <span class="nx">requestOptions</span><span class="p">)</span>
|
||||
<span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">response</span> <span class="o">=></span> <span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">())</span>
|
||||
<span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">result</span> <span class="o">=></span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">result</span><span class="p">))</span>
|
||||
<span class="p">.</span><span class="k">catch</span><span class="p">(</span><span class="nx">error</span> <span class="o">=></span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">error</span><span class="dl">'</span><span class="p">,</span> <span class="nx">error</span><span class="p">));</span>
|
||||
</code></pre></div><div class="highlight"><pre class="highlight ruby tab-ruby"><code><span class="nb">require</span> <span class="s2">"uri"</span>
|
||||
<span class="nb">require</span> <span class="s2">"json"</span>
|
||||
<span class="nb">require</span> <span class="s2">"net/http"</span>
|
||||
|
||||
<span class="n">url</span> <span class="o">=</span> <span class="no">URI</span><span class="p">(</span><span class="s2">"https://fleet-api.prd.na.vn.cloud.tesla.com/api/1/vehicles/{id}/command/actuate_trunk"</span><span class="p">)</span>
|
||||
|
||||
<span class="n">https</span> <span class="o">=</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">url</span><span class="p">.</span><span class="nf">host</span><span class="p">,</span> <span class="n">url</span><span class="p">.</span><span class="nf">port</span><span class="p">)</span>
|
||||
<span class="n">https</span><span class="p">.</span><span class="nf">use_ssl</span> <span class="o">=</span> <span class="kp">true</span>
|
||||
|
||||
<span class="n">request</span> <span class="o">=</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="o">::</span><span class="no">Post</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
|
||||
<span class="n">request</span><span class="p">[</span><span class="s2">"Content-Type"</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"application/json"</span>
|
||||
<span class="n">request</span><span class="p">[</span><span class="s2">"Authorization"</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"Bearer ENV_TESLA_API_TOKEN"</span>
|
||||
<span class="n">request</span><span class="p">.</span><span class="nf">body</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">dump</span><span class="p">({</span>
|
||||
<span class="s2">"which_trunk"</span><span class="p">:</span> <span class="s2">"string"</span>
|
||||
<span class="p">})</span>
|
||||
|
||||
<span class="n">response</span> <span class="o">=</span> <span class="n">https</span><span class="p">.</span><span class="nf">request</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
|
||||
<span class="nb">puts</span> <span class="n">response</span><span class="p">.</span><span class="nf">read_body</span>
|
||||
|
||||
</code></pre></div><div class="highlight"><pre class="highlight python tab-python"><code><span class="kn">import</span> <span class="nn">os</span>
|
||||
<span class="kn">import</span> <span class="nn">http.client</span>
|
||||
<span class="kn">import</span> <span class="nn">json</span>
|
||||
|
||||
<span class="n">conn</span> <span class="o">=</span> <span class="n">http</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">HTTPSConnection</span><span class="p">(</span><span class="s">"fleet-api.prd.na.vn.cloud.tesla.com"</span><span class="p">)</span>
|
||||
<span class="n">payload</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">({</span>
|
||||
<span class="s">"which_trunk"</span><span class="p">:</span> <span class="s">"string"</span>
|
||||
<span class="p">})</span>
|
||||
<span class="n">headers</span> <span class="o">=</span> <span class="p">{</span>
|
||||
<span class="s">'Content-Type'</span><span class="p">:</span> <span class="s">'application/json'</span><span class="p">,</span>
|
||||
<span class="s">'Authorization'</span><span class="p">:</span> <span class="s">'Bearer ENV_TESLA_API_TOKEN'</span>
|
||||
<span class="p">}</span>
|
||||
<span class="n">conn</span><span class="p">.</span><span class="n">request</span><span class="p">(</span><span class="s">"POST"</span><span class="p">,</span> <span class="s">"/api/1/vehicles/{id}/command/actuate_trunk"</span><span class="p">,</span> <span class="n">payload</span><span class="p">,</span> <span class="n">headers</span><span class="p">)</span>
|
||||
<span class="n">res</span> <span class="o">=</span> <span class="n">conn</span><span class="p">.</span><span class="n">getresponse</span><span class="p">()</span>
|
||||
<span class="n">data</span> <span class="o">=</span> <span class="n">res</span><span class="p">.</span><span class="n">read</span><span class="p">()</span>
|
||||
<span class="k">print</span><span class="p">(</span><span class="n">data</span><span class="p">.</span><span class="n">decode</span><span class="p">(</span><span class="s">"utf-8"</span><span class="p">))</span>
|
||||
</code></pre></div>
|
||||
<p>Controls the front (which_trunk: "front") or rear (which_trunk: "rear") trunk.</p>
|
||||
<h3 id='parameters-10'>Parameters</h3>
|
||||
<table><thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>In</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead><tbody>
|
||||
<tr>
|
||||
<td>id</td>
|
||||
<td>path</td>
|
||||
<td>integer</td>
|
||||
<td>Yes</td>
|
||||
<td>vehicle id</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>which_trunk</td>
|
||||
<td>body</td>
|
||||
<td>string</td>
|
||||
<td>Yes</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
<div class="highlight"><pre class="highlight json tab-json"><code><details><summary> Click to view successful response</summary><span class="p">{</span><span class="w">
|
||||
</span><span class="nl">"result"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
|
||||
</span><span class="nl">"reason"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="w">
|
||||
</span><span class="p">}</span><span class="w">
|
||||
</span></details></code></pre></div>
|
||||
|
||||
<h2 id='adjust_volume'>adjust_volume</h2>
|
||||
<p><span class="endpoint"><code>POST /api/1/vehicles/{id}/command/adjust_volume</code></span></p>
|
||||
|
||||
<p>scopes: <em>vehicle_cmds</em></p>
|
||||
<div class="highlight"><pre class="highlight shell tab-shell"><code>curl <span class="nt">--header</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
|
||||
<span class="nt">--header</span> <span class="s2">"Authorization: Bearer </span><span class="nv">$TESLA_API_TOKEN</span><span class="s2">"</span> <span class="se">\</span>
|
||||
<span class="nt">--data</span> <span class="s1">'{"volume":"integer"}'</span> <span class="se">\</span>
|
||||
<span class="s1">'https://fleet-api.prd.na.vn.cloud.tesla.com/api/1/vehicles/{id}/command/adjust_volume'</span>
|
||||
</code></pre></div><div class="highlight"><pre class="highlight javascript tab-javascript"><code><span class="kd">const</span> <span class="nx">myHeaders</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Headers</span><span class="p">();</span>
|
||||
<span class="nx">myHeaders</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">);</span>
|
||||
<span class="nx">myHeaders</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="dl">"</span><span class="s2">Authorization</span><span class="dl">"</span><span class="p">,</span> <span class="s2">`Bearer </span><span class="p">${</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">TESLA_API_TOKEN</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
|
||||
|
||||
<span class="kd">const</span> <span class="nx">body</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span>
|
||||
<span class="dl">"</span><span class="s2">volume</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">integer</span><span class="dl">"</span>
|
||||
<span class="p">});</span>
|
||||
|
||||
<span class="kd">const</span> <span class="nx">requestOptions</span> <span class="o">=</span> <span class="p">{</span>
|
||||
<span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
|
||||
<span class="na">headers</span><span class="p">:</span> <span class="nx">myHeaders</span><span class="p">,</span>
|
||||
<span class="nx">body</span><span class="p">,</span>
|
||||
<span class="na">redirect</span><span class="p">:</span> <span class="dl">'</span><span class="s1">follow</span><span class="dl">'</span>
|
||||
<span class="p">};</span>
|
||||
|
||||
<span class="nx">fetch</span><span class="p">(</span><span class="dl">"</span><span class="s2">https://fleet-api.prd.na.vn.cloud.tesla.com/api/1/vehicles/{id}/command/adjust_volume</span><span class="dl">"</span><span class="p">,</span> <span class="nx">requestOptions</span><span class="p">)</span>
|
||||
<span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">response</span> <span class="o">=></span> <span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">())</span>
|
||||
<span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">result</span> <span class="o">=></span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">result</span><span class="p">))</span>
|
||||
<span class="p">.</span><span class="k">catch</span><span class="p">(</span><span class="nx">error</span> <span class="o">=></span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">error</span><span class="dl">'</span><span class="p">,</span> <span class="nx">error</span><span class="p">));</span>
|
||||
</code></pre></div><div class="highlight"><pre class="highlight ruby tab-ruby"><code><span class="nb">require</span> <span class="s2">"uri"</span>
|
||||
<span class="nb">require</span> <span class="s2">"json"</span>
|
||||
<span class="nb">require</span> <span class="s2">"net/http"</span>
|
||||
|
||||
<span class="n">url</span> <span class="o">=</span> <span class="no">URI</span><span class="p">(</span><span class="s2">"https://fleet-api.prd.na.vn.cloud.tesla.com/api/1/vehicles/{id}/command/adjust_volume"</span><span class="p">)</span>
|
||||
|
||||
<span class="n">https</span> <span class="o">=</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">url</span><span class="p">.</span><span class="nf">host</span><span class="p">,</span> <span class="n">url</span><span class="p">.</span><span class="nf">port</span><span class="p">)</span>
|
||||
<span class="n">https</span><span class="p">.</span><span class="nf">use_ssl</span> <span class="o">=</span> <span class="kp">true</span>
|
||||
|
||||
<span class="n">request</span> <span class="o">=</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="o">::</span><span class="no">Post</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
|
||||
<span class="n">request</span><span class="p">[</span><span class="s2">"Content-Type"</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"application/json"</span>
|
||||
<span class="n">request</span><span class="p">[</span><span class="s2">"Authorization"</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"Bearer ENV_TESLA_API_TOKEN"</span>
|
||||
<span class="n">request</span><span class="p">.</span><span class="nf">body</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">dump</span><span class="p">({</span>
|
||||
<span class="s2">"volume"</span><span class="p">:</span> <span class="s2">"integer"</span>
|
||||
<span class="p">})</span>
|
||||
|
||||
<span class="n">response</span> <span class="o">=</span> <span class="n">https</span><span class="p">.</span><span class="nf">request</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
|
||||
<span class="nb">puts</span> <span class="n">response</span><span class="p">.</span><span class="nf">read_body</span>
|
||||
|
||||
</code></pre></div><div class="highlight"><pre class="highlight python tab-python"><code><span class="kn">import</span> <span class="nn">os</span>
|
||||
<span class="kn">import</span> <span class="nn">http.client</span>
|
||||
<span class="kn">import</span> <span class="nn">json</span>
|
||||
|
||||
<span class="n">conn</span> <span class="o">=</span> <span class="n">http</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">HTTPSConnection</span><span class="p">(</span><span class="s">"fleet-api.prd.na.vn.cloud.tesla.com"</span><span class="p">)</span>
|
||||
<span class="n">payload</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">({</span>
|
||||
<span class="s">"volume"</span><span class="p">:</span> <span class="s">"integer"</span>
|
||||
<span class="p">})</span>
|
||||
<span class="n">headers</span> <span class="o">=</span> <span class="p">{</span>
|
||||
<span class="s">'Content-Type'</span><span class="p">:</span> <span class="s">'application/json'</span><span class="p">,</span>
|
||||
<span class="s">'Authorization'</span><span class="p">:</span> <span class="s">'Bearer ENV_TESLA_API_TOKEN'</span>
|
||||
<span class="p">}</span>
|
||||
<span class="n">conn</span><span class="p">.</span><span class="n">request</span><span class="p">(</span><span class="s">"POST"</span><span class="p">,</span> <span class="s">"/api/1/vehicles/{id}/command/adjust_volume"</span><span class="p">,</span> <span class="n">payload</span><span class="p">,</span> <span class="n">headers</span><span class="p">)</span>
|
||||
<span class="n">res</span> <span class="o">=</span> <span class="n">conn</span><span class="p">.</span><span class="n">getresponse</span><span class="p">()</span>
|
||||
<span class="n">data</span> <span class="o">=</span> <span class="n">res</span><span class="p">.</span><span class="n">read</span><span class="p">()</span>
|
||||
<span class="k">print</span><span class="p">(</span><span class="n">data</span><span class="p">.</span><span class="n">decode</span><span class="p">(</span><span class="s">"utf-8"</span><span class="p">))</span>
|
||||
</code></pre></div>
|
||||
<p>Adjusts vehicle media playback volume.</p>
|
||||
<h3 id='parameters-11'>Parameters</h3>
|
||||
<table><thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>In</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead><tbody>
|
||||
<tr>
|
||||
<td>id</td>
|
||||
<td>path</td>
|
||||
<td>integer</td>
|
||||
<td>Yes</td>
|
||||
<td>vehicle id</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>volume</td>
|
||||
<td>body</td>
|
||||
<td>integer</td>
|
||||
<td>Yes</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
<div class="highlight"><pre class="highlight json tab-json"><code><details><summary> Click to view successful response</summary><span class="p">{</span><span class="w">
|
||||
</span><span class="nl">"result"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
|
||||
</span><span class="nl">"reason"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="w">
|
||||
</span><span class="p">}</span><span class="w">
|
||||
</span></details></code></pre></div>
|
||||
|
||||
*/
|
||||
|
||||
fn parse(html: &str) -> FleetApiSpec {
|
||||
let document = Html::parse_document(html);
|
||||
let content_selector = selector(".content h1");
|
||||
let mut element = document.select(&content_selector).next().unwrap();
|
||||
let mut category = None;
|
||||
|
||||
// Iterate over all the elements in the content section until we see a h1 or h2.
|
||||
loop {
|
||||
match element.value().name() {
|
||||
"h1" => {
|
||||
let category_name = element.value().id().unwrap();
|
||||
category = Category::from_str(&category_name).ok();
|
||||
}
|
||||
"h2" => {
|
||||
if category.is_some() {
|
||||
let name = element.inner_html();
|
||||
println!("{category:?} {name:?}");
|
||||
// let call = parse_call(element);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let Some(next_element) = element.next_sibling_element() else {
|
||||
println!("exiting...");
|
||||
break;
|
||||
};
|
||||
element = next_element;
|
||||
}
|
||||
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Return None if this is not an endpoint.
|
||||
///
|
||||
/// Will panic if it looks like an endpoint and has trouble parsing.
|
||||
fn parse_call(element: ElementRef) -> Option<Call> {
|
||||
let name = element.value().id().unwrap();
|
||||
|
||||
// <p><span class="endpoint"><code>POST /api/1/vehicles/{id}/command/auto_conditioning_start</code></span></p>
|
||||
// This section determines if this is an endpoint or not.
|
||||
let (fragment, element) = next(element);
|
||||
let url = fragment.select(&selector("code")).next()?.inner_html();
|
||||
if !url.starts_with("GET ") && !url.starts_with("POST ") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (method, url) = url.split_once(' ').unwrap();
|
||||
println!("{} {}", method, url);
|
||||
|
||||
// <p>scopes: <em>vehicle_cmds</em></p>
|
||||
let (fragment, element) = next(element);
|
||||
let scopes = fragment
|
||||
.select(&selector("em"))
|
||||
.map(|e| e.inner_html())
|
||||
.map(|e| Scope::from_str(&e))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// 4 <div class="highlight"> nodes containing example requests in different languages.
|
||||
// TODO: Skip for now
|
||||
let mut count = 0;
|
||||
let mut element = element;
|
||||
loop {
|
||||
let (fragment, new_element) = next(element);
|
||||
element = new_element;
|
||||
if fragment
|
||||
.select(&selector(r#"div[class="highlight"]"#))
|
||||
.next()
|
||||
.is_none()
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
count += 1;
|
||||
if count == 10 {
|
||||
panic!("Too many examples");
|
||||
}
|
||||
}
|
||||
if count == 0 && name != "api-status" {
|
||||
panic!("No examples for {}", name);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn next(element: ElementRef) -> (Html, ElementRef) {
|
||||
let element = element.next_sibling_element().unwrap();
|
||||
let html = Html::parse_fragment(&element.html());
|
||||
(html, element)
|
||||
}
|
||||
|
||||
fn selector(s: &str) -> Selector {
|
||||
Selector::parse(s).unwrap()
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "scrape_fleet_api"
|
||||
name = "tesla_api_coverage"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
|
@ -7,6 +7,10 @@ edition = "2021"
|
|||
reqwest = "0.11.22"
|
||||
tokio = { version = "1.33.0", features = ["full"] }
|
||||
clap = { version = "4.4.6", features = ["derive"] }
|
||||
tracing-subscriber = "0.3.17"
|
||||
scraper = "0.17.1"
|
||||
strum = { version = "0.25.0", features = ["derive"] }
|
||||
tracing-subscriber = "0.3.17"
|
||||
tracing = "0.1.40"
|
||||
log = "0.4.20"
|
||||
nom = "7.1.3"
|
||||
anyhow = "1.0.75"
|
42
tesla_api_coverage/README.md
Normal file
42
tesla_api_coverage/README.md
Normal file
|
@ -0,0 +1,42 @@
|
|||
# API Coverage
|
||||
|
||||
A tool designed to compare the API calls between `teslatte` and the publicly documented Tesla APIs.
|
||||
|
||||
**Note:** This tool is bespoke to the build of `teslatte` and is not intended for publishing on crates.io.
|
||||
|
||||
This project does (or will do) the following:
|
||||
|
||||
* Scrape the teslatte project for what has been implemented.
|
||||
* Scrape the Fleet API for its list of endpoints.
|
||||
* Scrape the Command Mode SDK sources for its list of endpoints: https://github.com/teslamotors/vehicle-command/blob/main/cmd/tesla-control/commands.go
|
||||
* Scrape timdorr/tesla-api's endpoints file: https://github.com/timdorr/tesla-api/blob/master/ownerapi_endpoints.json
|
||||
* Combine the results into a single list of endpoints.
|
||||
* Has a configuration on how to merge the endpoints, e.g. if an endpoint name is different, how to resolve it.
|
||||
* Output a table of endpoints that are implemented or not, maybe in Markdown.
|
||||
|
||||
|
||||
### Brainstorm
|
||||
|
||||
Combined format idea:
|
||||
|
||||
```json
|
||||
{
|
||||
"honk_horn": {
|
||||
|
||||
// If owner-api vs fleet-api methods are different, they should have different entries,
|
||||
// otherwise call it "rest":
|
||||
"rest": {
|
||||
"method": "POST",
|
||||
"endpoint": "/vehicles/{vehicle_id}/command/honk_horn"
|
||||
},
|
||||
"vehicle-command": {
|
||||
"endpoint": "honk"
|
||||
}
|
||||
|
||||
"timdorr-endpoints-file": true,
|
||||
"teslatte": true,
|
||||
"owners-api": true,
|
||||
"fleet-api": true,
|
||||
}
|
||||
}
|
||||
```
|
|
@ -2248,9 +2248,7 @@ The response body is typically JSON-encoded with details in the "error"
|
|||
<span class="nt">--header</span> <span class="s2">"Authorization: Bearer </span><span class="nv">$TESLA_API_TOKEN</span><span class="s2">"</span> <span class="se">\</span>
|
||||
<span class="nt">--data</span> <span class="s1">'{}'</span> <span class="se">\</span>
|
||||
<span class="s1">'https://fleet-api.prd.na.vn.cloud.tesla.com/api/1/vehicles/{id}/command/auto_conditioning_start'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<div class="highlight"><pre class="highlight javascript tab-javascript"><code><span class="kd">const</span> <span class="nx">myHeaders</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Headers</span><span class="p">();</span>
|
||||
</code></pre></div><div class="highlight"><pre class="highlight javascript tab-javascript"><code><span class="kd">const</span> <span class="nx">myHeaders</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Headers</span><span class="p">();</span>
|
||||
<span class="nx">myHeaders</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">);</span>
|
||||
<span class="nx">myHeaders</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="dl">"</span><span class="s2">Authorization</span><span class="dl">"</span><span class="p">,</span> <span class="s2">`Bearer </span><span class="p">${</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">TESLA_API_TOKEN</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
|
||||
|
||||
|
@ -2267,9 +2265,7 @@ The response body is typically JSON-encoded with details in the "error"
|
|||
<span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">response</span> <span class="o">=></span> <span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">())</span>
|
||||
<span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">result</span> <span class="o">=></span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">result</span><span class="p">))</span>
|
||||
<span class="p">.</span><span class="k">catch</span><span class="p">(</span><span class="nx">error</span> <span class="o">=></span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">error</span><span class="dl">'</span><span class="p">,</span> <span class="nx">error</span><span class="p">));</span>
|
||||
</code></pre></div>
|
||||
|
||||
<div class="highlight"><pre class="highlight ruby tab-ruby"><code><span class="nb">require</span> <span class="s2">"uri"</span>
|
||||
</code></pre></div><div class="highlight"><pre class="highlight ruby tab-ruby"><code><span class="nb">require</span> <span class="s2">"uri"</span>
|
||||
<span class="nb">require</span> <span class="s2">"json"</span>
|
||||
<span class="nb">require</span> <span class="s2">"net/http"</span>
|
||||
|
||||
|
@ -2301,7 +2297,6 @@ The response body is typically JSON-encoded with details in the "error"
|
|||
<span class="n">data</span> <span class="o">=</span> <span class="n">res</span><span class="p">.</span><span class="n">read</span><span class="p">()</span>
|
||||
<span class="k">print</span><span class="p">(</span><span class="n">data</span><span class="p">.</span><span class="n">decode</span><span class="p">(</span><span class="s">"utf-8"</span><span class="p">))</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Starts climate preconditioning.</p>
|
||||
<h3 id='parameters-12'>Parameters</h3>
|
||||
<table><thead>
|
2761
tesla_api_coverage/cached/timdorr.json
Normal file
2761
tesla_api_coverage/cached/timdorr.json
Normal file
File diff suppressed because it is too large
Load diff
695
tesla_api_coverage/cached/vehicle_command.go
Normal file
695
tesla_api_coverage/cached/vehicle_command.go
Normal file
|
@ -0,0 +1,695 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/teslamotors/vehicle-command/pkg/account"
|
||||
"github.com/teslamotors/vehicle-command/pkg/cli"
|
||||
"github.com/teslamotors/vehicle-command/pkg/protocol"
|
||||
"github.com/teslamotors/vehicle-command/pkg/protocol/protobuf/vcsec"
|
||||
"github.com/teslamotors/vehicle-command/pkg/vehicle"
|
||||
)
|
||||
|
||||
var ErrCommandLineArgs = errors.New("invalid command line arguments")
|
||||
|
||||
type Argument struct {
|
||||
name string
|
||||
help string
|
||||
}
|
||||
|
||||
type Handler func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error
|
||||
|
||||
type Command struct {
|
||||
help string
|
||||
requiresAuth bool // True if command requires client-to-vehicle authentication (private key)
|
||||
requiresFleetAPI bool // True if command requires client-to-server authentication (OAuth token)
|
||||
args []Argument
|
||||
optional []Argument
|
||||
handler Handler
|
||||
}
|
||||
|
||||
// configureAndVerifyFlags verifies that c contains all the information required to execute a command.
|
||||
func configureFlags(c *cli.Config, commandName string, forceBLE bool) error {
|
||||
info, ok := commands[commandName]
|
||||
if !ok {
|
||||
return ErrUnknownCommand
|
||||
}
|
||||
c.Flags = cli.FlagBLE
|
||||
if info.requiresAuth {
|
||||
c.Flags |= cli.FlagPrivateKey | cli.FlagVIN
|
||||
}
|
||||
if !info.requiresFleetAPI {
|
||||
c.Flags |= cli.FlagVIN
|
||||
}
|
||||
if forceBLE {
|
||||
if info.requiresFleetAPI {
|
||||
return ErrRequiresOAuth
|
||||
}
|
||||
} else {
|
||||
c.Flags |= cli.FlagOAuth
|
||||
}
|
||||
|
||||
// Verify all required parameters are present.
|
||||
havePrivateKey := !(c.KeyringKeyName == "" && c.KeyFilename == "")
|
||||
haveOAuth := !(c.KeyringTokenName == "" && c.TokenFilename == "")
|
||||
haveVIN := c.VIN != ""
|
||||
_, err := checkReadiness(commandName, havePrivateKey, haveOAuth, haveVIN)
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
ErrRequiresOAuth = errors.New("command requires a FleetAPI OAuth token")
|
||||
ErrRequiresVIN = errors.New("command requires a VIN")
|
||||
ErrRequiresPrivateKey = errors.New("command requires a private key")
|
||||
ErrUnknownCommand = errors.New("unrecognized command")
|
||||
)
|
||||
|
||||
func checkReadiness(commandName string, havePrivateKey, haveOAuth, haveVIN bool) (*Command, error) {
|
||||
info, ok := commands[commandName]
|
||||
if !ok {
|
||||
return nil, ErrUnknownCommand
|
||||
}
|
||||
if info.requiresFleetAPI {
|
||||
if !haveOAuth {
|
||||
return nil, ErrRequiresOAuth
|
||||
}
|
||||
} else {
|
||||
// Currently, commands supported by this application either target the account (and
|
||||
// therefore require FleetAPI credentials but not a VIN) or target a vehicle (and therefore
|
||||
// require a VIN but not FleetAPI credentials).
|
||||
if !haveVIN {
|
||||
return nil, ErrRequiresVIN
|
||||
}
|
||||
}
|
||||
if info.requiresAuth && !havePrivateKey {
|
||||
return nil, ErrRequiresPrivateKey
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func execute(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return errors.New("missing COMMAND")
|
||||
}
|
||||
|
||||
info, err := checkReadiness(args[0], car != nil && car.PrivateKeyAvailable(), acct != nil, car != nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(args)-1 < len(info.args) || len(args)-1 > len(info.args)+len(info.optional) {
|
||||
writeErr("Invalid number of command line arguments: %d (%d required, %d optional).", len(args), len(info.args), len(info.optional))
|
||||
err = ErrCommandLineArgs
|
||||
} else {
|
||||
keywords := make(map[string]string)
|
||||
for i, argInfo := range info.args {
|
||||
keywords[argInfo.name] = args[i+1]
|
||||
}
|
||||
index := len(info.args) + 1
|
||||
for _, argInfo := range info.optional {
|
||||
if index >= len(args) {
|
||||
break
|
||||
}
|
||||
keywords[argInfo.name] = args[index]
|
||||
index++
|
||||
}
|
||||
err = info.handler(ctx, acct, car, keywords)
|
||||
}
|
||||
|
||||
// Print command-specific help
|
||||
if errors.Is(err, ErrCommandLineArgs) {
|
||||
info.Usage(args[0])
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Command) Usage(name string) {
|
||||
fmt.Printf("Usage: %s", name)
|
||||
maxLength := 0
|
||||
for _, arg := range c.args {
|
||||
fmt.Printf(" %s", arg.name)
|
||||
if len(arg.name) > maxLength {
|
||||
maxLength = len(arg.name)
|
||||
}
|
||||
}
|
||||
if len(c.optional) > 0 {
|
||||
fmt.Printf(" [")
|
||||
}
|
||||
for _, arg := range c.optional {
|
||||
fmt.Printf(" %s", arg.name)
|
||||
if len(arg.name) > maxLength {
|
||||
maxLength = len(arg.name)
|
||||
}
|
||||
}
|
||||
if len(c.optional) > 0 {
|
||||
fmt.Printf(" ]")
|
||||
}
|
||||
fmt.Printf("\n%s\n", c.help)
|
||||
maxLength++
|
||||
for _, arg := range c.args {
|
||||
fmt.Printf(" %s:%s%s\n", arg.name, strings.Repeat(" ", maxLength-len(arg.name)), arg.help)
|
||||
}
|
||||
for _, arg := range c.optional {
|
||||
fmt.Printf(" %s:%s%s\n", arg.name, strings.Repeat(" ", maxLength-len(arg.name)), arg.help)
|
||||
}
|
||||
}
|
||||
|
||||
var commands = map[string]*Command{
|
||||
"unlock": &Command{
|
||||
help: "Unlock vehicle",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
return car.Unlock(ctx)
|
||||
},
|
||||
},
|
||||
"lock": &Command{
|
||||
help: "Lock vehicle",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
return car.Lock(ctx)
|
||||
},
|
||||
},
|
||||
"drive": &Command{
|
||||
help: "Remote start vehicle",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
return car.RemoteDrive(ctx)
|
||||
},
|
||||
},
|
||||
"climate-on": &Command{
|
||||
help: "Turn on climate control",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
return car.ClimateOn(ctx)
|
||||
},
|
||||
},
|
||||
"climate-off": &Command{
|
||||
help: "Turn off climate control",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
return car.ClimateOff(ctx)
|
||||
},
|
||||
},
|
||||
"climate-set-temp": &Command{
|
||||
help: "Set temperature (Celsius)",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
args: []Argument{
|
||||
Argument{name: "TEMP", help: "Desired temperature (e.g., 70f or 21c; defaults to Celsius)"},
|
||||
},
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
var degrees float32
|
||||
var unit string
|
||||
if _, err := fmt.Sscanf(args["TEMP"], "%f%s", °rees, &unit); err != nil {
|
||||
return fmt.Errorf("failed to parse temperature: format as 22C or 72F")
|
||||
}
|
||||
if unit == "F" || unit == "f" {
|
||||
degrees = (5.0 * degrees / 9.0) + 32.0
|
||||
} else if unit != "C" && unit != "c" {
|
||||
return fmt.Errorf("temperature units must be C or F")
|
||||
}
|
||||
return car.ChangeClimateTemp(ctx, degrees, degrees)
|
||||
},
|
||||
},
|
||||
"add-key": &Command{
|
||||
help: "Add PUBLIC_KEY to vehicle whitelist with ROLE and FORM_FACTOR",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
args: []Argument{
|
||||
Argument{name: "PUBLIC_KEY", help: "file containing public key (or corresponding private key)"},
|
||||
Argument{name: "ROLE", help: "One of: owner, driver"},
|
||||
Argument{name: "FORM_FACTOR", help: "One of: nfc_card, ios_device, android_device, cloud_key"},
|
||||
},
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
role := strings.ToUpper(args["ROLE"])
|
||||
if role != "OWNER" && role != "DRIVER" {
|
||||
return fmt.Errorf("%w: invalid ROLE", ErrCommandLineArgs)
|
||||
}
|
||||
formFactor, ok := vcsec.KeyFormFactor_value["KEY_FORM_FACTOR_"+strings.ToUpper(args["FORM_FACTOR"])]
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: unrecognized FORM_FACTOR", ErrCommandLineArgs)
|
||||
}
|
||||
publicKey, err := protocol.LoadPublicKey(args["PUBLIC_KEY"])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid public key: %s", err)
|
||||
}
|
||||
return car.AddKey(ctx, publicKey, role == "OWNER", vcsec.KeyFormFactor(formFactor))
|
||||
},
|
||||
},
|
||||
"add-key-request": &Command{
|
||||
help: "Requset NFC-card approval for a enrolling PUBLIC_KEY with ROLE and FORM_FACTOR",
|
||||
requiresAuth: false,
|
||||
requiresFleetAPI: false,
|
||||
args: []Argument{
|
||||
Argument{name: "PUBLIC_KEY", help: "file containing public key (or corresponding private key)"},
|
||||
Argument{name: "ROLE", help: "One of: owner, driver"},
|
||||
Argument{name: "FORM_FACTOR", help: "One of: nfc_card, ios_device, android_device, cloud_key"},
|
||||
},
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
role := strings.ToUpper(args["ROLE"])
|
||||
if role != "OWNER" && role != "DRIVER" {
|
||||
return fmt.Errorf("%w: invalid ROLE", ErrCommandLineArgs)
|
||||
}
|
||||
formFactor, ok := vcsec.KeyFormFactor_value["KEY_FORM_FACTOR_"+strings.ToUpper(args["FORM_FACTOR"])]
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: unrecognized FORM_FACTOR", ErrCommandLineArgs)
|
||||
}
|
||||
publicKey, err := protocol.LoadPublicKey(args["PUBLIC_KEY"])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid public key: %s", err)
|
||||
}
|
||||
if err := car.SendAddKeyRequest(ctx, publicKey, role == "OWNER", vcsec.KeyFormFactor(formFactor)); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Sent add-key request to %s. Confirm by tapping NFC card on center console.\n", car.VIN())
|
||||
return nil
|
||||
},
|
||||
},
|
||||
"remove-key": &Command{
|
||||
help: "Remove PUBLIC_KEY from vehicle whitelist",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
args: []Argument{
|
||||
Argument{name: "PUBLIC_KEY", help: "file containing public key (or corresponding private key)"},
|
||||
},
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
publicKey, err := protocol.LoadPublicKey(args["PUBLIC_KEY"])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid public key: %s", err)
|
||||
}
|
||||
return car.RemoveKey(ctx, publicKey)
|
||||
},
|
||||
},
|
||||
"rename-key": &Command{
|
||||
help: "Change the human-readable metadata of PUBLIC_KEY to NAME, MODEL, KIND",
|
||||
requiresAuth: false,
|
||||
requiresFleetAPI: true,
|
||||
args: []Argument{
|
||||
Argument{name: "PUBLIC_KEY", help: "file containing public key (or corresponding private key)"},
|
||||
Argument{name: "NAME", help: "New human-readable name for the public key (e.g., Dave's Phone)"},
|
||||
},
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
publicKey, err := protocol.LoadPublicKey(args["PUBLIC_KEY"])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid public key: %s", err)
|
||||
}
|
||||
return acct.UpdateKey(ctx, publicKey, args["NAME"])
|
||||
},
|
||||
},
|
||||
"get": &Command{
|
||||
help: "GET an owner API http ENDPOINT. Hostname will be taken from -config.",
|
||||
requiresAuth: false,
|
||||
requiresFleetAPI: true,
|
||||
args: []Argument{
|
||||
Argument{name: "ENDPOINT", help: "Fleet API endpoint"},
|
||||
},
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
reply, err := acct.Get(ctx, args["ENDPOINT"])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(reply))
|
||||
return nil
|
||||
},
|
||||
},
|
||||
"post": &Command{
|
||||
help: "POST to ENDPOINT the contents of FILE. Hostname will be taken from -config.",
|
||||
requiresAuth: false,
|
||||
requiresFleetAPI: true,
|
||||
args: []Argument{
|
||||
Argument{name: "ENDPOINT", help: "Fleet API endpoint"},
|
||||
},
|
||||
optional: []Argument{
|
||||
Argument{name: "FILE", help: "JSON file to POST"},
|
||||
},
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
var jsonBytes []byte
|
||||
var err error
|
||||
if filename, ok := args["FILE"]; ok {
|
||||
jsonBytes, err = os.ReadFile(filename)
|
||||
} else {
|
||||
jsonBytes, err = io.ReadAll(os.Stdin)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply, err := acct.Post(ctx, args["ENDPOINT"], jsonBytes)
|
||||
// reply can be set where there's an error; typically a JSON blob providing details
|
||||
if reply != nil {
|
||||
fmt.Println(string(reply))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
"list-keys": &Command{
|
||||
help: "List public keys enrolled on vehicle",
|
||||
requiresAuth: false,
|
||||
requiresFleetAPI: false,
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
summary, err := car.KeySummary(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
slot := uint32(0)
|
||||
var details *vcsec.WhitelistEntryInfo
|
||||
for mask := summary.GetSlotMask(); mask > 0; mask >>= 1 {
|
||||
if mask&1 == 1 {
|
||||
details, err = car.KeyInfoBySlot(ctx, slot)
|
||||
if err != nil {
|
||||
writeErr("Error fetching slot %d: %s", slot, err)
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if details != nil {
|
||||
fmt.Printf("%02x\t%s\t%s\n", details.GetPublicKey().GetPublicKeyRaw(), details.GetKeyRole(), details.GetMetadataForKey().GetKeyFormFactor())
|
||||
}
|
||||
}
|
||||
slot++
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
"honk": &Command{
|
||||
help: "Honk horn",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
return car.HonkHorn(ctx)
|
||||
},
|
||||
},
|
||||
"ping": &Command{
|
||||
help: "Ping vehicle",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
return car.Ping(ctx)
|
||||
},
|
||||
},
|
||||
"flash-lights": &Command{
|
||||
help: "Flash lights",
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
return car.FlashLights(ctx)
|
||||
},
|
||||
},
|
||||
"charging-set-limit": &Command{
|
||||
help: "Set charge limit to PERCENT",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
args: []Argument{
|
||||
Argument{name: "PERCENT", help: "Charging limit"},
|
||||
},
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
limit, err := strconv.Atoi(args["PERCENT"])
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing PERCENT")
|
||||
}
|
||||
return car.ChangeChargeLimit(ctx, int32(limit))
|
||||
},
|
||||
},
|
||||
"charging-start": &Command{
|
||||
help: "Start charging",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
return car.ChargeStart(ctx)
|
||||
},
|
||||
},
|
||||
"charging-stop": &Command{
|
||||
help: "Stop charging",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
return car.ChargeStop(ctx)
|
||||
},
|
||||
},
|
||||
"media-set-volume": &Command{
|
||||
help: "Set volume",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
args: []Argument{
|
||||
Argument{name: "VOLUME", help: "Set volume (0.0-10.0"},
|
||||
},
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
volume, err := strconv.ParseFloat(args["VOLUME"], 32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse volume")
|
||||
}
|
||||
return car.SetVolume(ctx, float32(volume))
|
||||
},
|
||||
},
|
||||
"software-update-start": &Command{
|
||||
help: "Start software update after DELAY",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
args: []Argument{
|
||||
Argument{
|
||||
name: "DELAY",
|
||||
help: "Time to wait before starting update. Examples: 2h, 10m.",
|
||||
},
|
||||
},
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
delay, err := time.ParseDuration(args["DELAY"])
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing DELAY. Valid times are <n><unit>, where <n> is a number (decimals are allowed) and <unit> is 's, 'm', or 'h'")
|
||||
// ...or 'ns'/'µs' if that's your cup of tea.
|
||||
}
|
||||
return car.ScheduleSoftwareUpdate(ctx, delay)
|
||||
},
|
||||
},
|
||||
"software-update-cancel": &Command{
|
||||
help: "Cancel a pending software update",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
return car.CancelSoftwareUpdate(ctx)
|
||||
},
|
||||
},
|
||||
"sentry-mode": &Command{
|
||||
help: "Set sentry mode to STATE ('on' or 'off')",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
args: []Argument{
|
||||
Argument{name: "STATE", help: "'on' or 'off'"},
|
||||
},
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
var state bool
|
||||
switch args["STATE"] {
|
||||
case "on":
|
||||
state = true
|
||||
case "off":
|
||||
state = false
|
||||
default:
|
||||
return fmt.Errorf("sentry mode state must be 'on' or 'off'")
|
||||
}
|
||||
return car.SetSentryMode(ctx, state)
|
||||
},
|
||||
},
|
||||
"wake": &Command{
|
||||
help: "Wake up vehicle",
|
||||
requiresAuth: false,
|
||||
requiresFleetAPI: false,
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
return car.Wakeup(ctx)
|
||||
},
|
||||
},
|
||||
"trunk-open": &Command{
|
||||
help: "Open vehicle trunk. Note that trunk-close only works on certain vehicle types.",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
return car.OpenTrunk(ctx)
|
||||
},
|
||||
},
|
||||
"trunk-move": &Command{
|
||||
help: "Toggle trunk open/closed. Closing is only available on certain vehicle types.",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
return car.ActuateTrunk(ctx)
|
||||
},
|
||||
},
|
||||
"trunk-close": &Command{
|
||||
help: "Closes vehicle trunk. Only available on certain vehicle types.",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
return car.CloseTrunk(ctx)
|
||||
},
|
||||
},
|
||||
"frunk-open": &Command{
|
||||
help: "Open vehicle frunk. Note that there's no frunk-close command!",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
return car.OpenFrunk(ctx)
|
||||
},
|
||||
},
|
||||
"charge-port-open": &Command{
|
||||
help: "Open charge port",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
return car.OpenChargePort(ctx)
|
||||
},
|
||||
},
|
||||
"charge-port-close": &Command{
|
||||
help: "Close charge port",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
return car.CloseChargePort(ctx)
|
||||
},
|
||||
},
|
||||
"autosecure-modelx": &Command{
|
||||
help: "Close falcon-wing doors and lock vehicle. Model X only.",
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
return car.AutoSecureVehicle(ctx)
|
||||
},
|
||||
},
|
||||
"session-info": &Command{
|
||||
help: "Retrieve session info for PUBLIC_KEY from DOMAIN",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
args: []Argument{
|
||||
Argument{name: "PUBLIC_KEY", help: "file containing public key (or corresponding private key)"},
|
||||
Argument{name: "DOMAIN", help: "'vcsec' or 'infotainment'"},
|
||||
},
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
// See SeatPosition definition for controlling backrest heaters (limited models).
|
||||
domains := map[string]protocol.Domain{
|
||||
"vcsec": protocol.DomainVCSEC,
|
||||
"infotainment": protocol.DomainInfotainment,
|
||||
}
|
||||
domain, ok := domains[args["DOMAIN"]]
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid domain %s", args["DOMAIN"])
|
||||
}
|
||||
publicKey, err := protocol.LoadPublicKey(args["PUBLIC_KEY"])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid public key: %s", err)
|
||||
}
|
||||
info, err := car.SessionInfo(ctx, publicKey, domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("%s\n", info)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
"seat-heater": &Command{
|
||||
help: "Set seat heater at POSITION to LEVEL",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
args: []Argument{
|
||||
Argument{name: "SEAT", help: "<front|2nd-row|3rd-row>-<left|center|right> (e.g., 2nd-row-left)"},
|
||||
Argument{name: "LEVEL", help: "off, low, medium, or high"},
|
||||
},
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
// See SeatPosition definition for controlling backrest heaters (limited models).
|
||||
seats := map[string]vehicle.SeatPosition{
|
||||
"front-left": vehicle.SeatFrontLeft,
|
||||
"front-right": vehicle.SeatFrontRight,
|
||||
"2nd-row-left": vehicle.SeatSecondRowLeft,
|
||||
"2nd-row-center": vehicle.SeatSecondRowCenter,
|
||||
"2nd-row-right": vehicle.SeatSecondRowRight,
|
||||
"3rd-row-left": vehicle.SeatThirdRowLeft,
|
||||
"3rd-row-right": vehicle.SeatThirdRowRight,
|
||||
}
|
||||
position, ok := seats[args["SEAT"]]
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid seat position")
|
||||
}
|
||||
levels := map[string]vehicle.Level{
|
||||
"off": vehicle.LevelOff,
|
||||
"low": vehicle.LevelLow,
|
||||
"medium": vehicle.LevelMed,
|
||||
"high": vehicle.LevelHigh,
|
||||
}
|
||||
level, ok := levels[args["LEVEL"]]
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid seat heater level")
|
||||
}
|
||||
spec := map[vehicle.SeatPosition]vehicle.Level{
|
||||
position: level,
|
||||
}
|
||||
return car.SetSeatHeater(ctx, spec)
|
||||
},
|
||||
},
|
||||
"steering-wheel-heater": &Command{
|
||||
help: "Set steering wheel mode to STATE ('on' or 'off')",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
args: []Argument{
|
||||
Argument{name: "STATE", help: "'on' or 'off'"},
|
||||
},
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
var state bool
|
||||
switch args["STATE"] {
|
||||
case "on":
|
||||
state = true
|
||||
case "off":
|
||||
state = false
|
||||
default:
|
||||
return fmt.Errorf("steering wheel state must be 'on' or 'off'")
|
||||
}
|
||||
return car.SetSteeringWheelHeater(ctx, state)
|
||||
},
|
||||
},
|
||||
"product-info": &Command{
|
||||
help: "Print JSON product info",
|
||||
requiresAuth: false,
|
||||
requiresFleetAPI: true,
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
productsJSON, err := acct.Get(ctx, "api/1/products")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(productsJSON))
|
||||
return nil
|
||||
},
|
||||
},
|
||||
"auto-seat-and-climate": &Command{
|
||||
help: "Turn on automatic seat heating and HVAC",
|
||||
requiresAuth: true,
|
||||
requiresFleetAPI: false,
|
||||
args: []Argument{
|
||||
Argument{name: "POSITIONS", help: "'L' (left), 'R' (right), or 'LR'"},
|
||||
},
|
||||
optional: []Argument{
|
||||
Argument{name: "STATE", help: "'on' (default) or 'off'"},
|
||||
},
|
||||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
var positions []vehicle.SeatPosition
|
||||
if strings.Contains(args["POSITIONS"], "L") {
|
||||
positions = append(positions, vehicle.SeatFrontLeft)
|
||||
}
|
||||
if strings.Contains(args["POSITIONS"], "R") {
|
||||
positions = append(positions, vehicle.SeatFrontRight)
|
||||
}
|
||||
if len(positions) != len(args["POSITIONS"]) {
|
||||
return fmt.Errorf("invalid seat position")
|
||||
}
|
||||
enabled := true
|
||||
if state, ok := args["STATE"]; ok && strings.ToUpper(state) == "OFF" {
|
||||
enabled = false
|
||||
}
|
||||
return car.AutoSeatAndClimate(ctx, positions, enabled)
|
||||
},
|
||||
},
|
||||
}
|
170
tesla_api_coverage/src/fleet.rs
Normal file
170
tesla_api_coverage/src/fleet.rs
Normal file
|
@ -0,0 +1,170 @@
|
|||
use scraper::{Element, ElementRef, Html, Selector};
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
|
||||
struct FleetApiSpec {
|
||||
calls: HashMap<String, Call>,
|
||||
}
|
||||
|
||||
// e.g. serialize to similar: vehicle-endpoints
|
||||
#[derive(Debug, strum::EnumString)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
enum Category {
|
||||
ChargingEndpoints,
|
||||
PartnerEndpoints,
|
||||
UserEndpoints,
|
||||
VehicleCommands,
|
||||
VehicleEndpoints,
|
||||
}
|
||||
|
||||
#[derive(Debug, strum::EnumString)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
enum Scope {
|
||||
/// Profile Information
|
||||
///
|
||||
/// Contact information, home address, profile picture, and referral information.
|
||||
UserData,
|
||||
|
||||
/// Vehicle Information
|
||||
///
|
||||
/// Vehicle live data, location, eligible upgrades, nearby superchargers, ownership, and service scheduling data.
|
||||
VehicleDeviceData,
|
||||
|
||||
/// Vehicle Commands
|
||||
///
|
||||
/// Commands like add/remove driver, access Live Camera, unlock, wake up, remote start, and schedule software updates.
|
||||
VehicleCmds,
|
||||
|
||||
/// Vehicle Charging Management
|
||||
///
|
||||
/// Vehicle charging history, billed amount, charging location, commands to schedule, and start/stop charging.
|
||||
VehicleChargingCmds,
|
||||
|
||||
/// Energy Product Information
|
||||
///
|
||||
/// Energy flow history, saving forecast, tariff rates, grid import, calendar, site status, time of use, and ownership.
|
||||
EnergyDeviceData,
|
||||
|
||||
/// Energy Product Commands
|
||||
///
|
||||
/// Commands like update storm mode.
|
||||
EnergyCmds,
|
||||
}
|
||||
|
||||
enum InRequestData {
|
||||
Query,
|
||||
Body,
|
||||
}
|
||||
|
||||
struct Parameter {
|
||||
name: String,
|
||||
request: InRequestData,
|
||||
var_type: String,
|
||||
required: bool,
|
||||
description: String,
|
||||
}
|
||||
|
||||
struct Call {
|
||||
name: String,
|
||||
method: reqwest::Method,
|
||||
url_definition: String,
|
||||
description: String,
|
||||
category: Category,
|
||||
scopes: Vec<Scope>,
|
||||
parameters: Vec<Parameter>,
|
||||
request_example: String,
|
||||
response_example: String,
|
||||
}
|
||||
|
||||
pub fn parse(html: &str) -> () {
|
||||
let document = Html::parse_document(html);
|
||||
let content_selector = selector(".content h1");
|
||||
let mut element = document.select(&content_selector).next().unwrap();
|
||||
let mut category = None;
|
||||
|
||||
// Iterate over all the elements in the content section until we see a h1 or h2.
|
||||
loop {
|
||||
match element.value().name() {
|
||||
"h1" => {
|
||||
let category_name = element.value().id().unwrap();
|
||||
category = Category::from_str(&category_name).ok();
|
||||
}
|
||||
"h2" => {
|
||||
if category.is_some() {
|
||||
let name = element.inner_html();
|
||||
println!("{category:?} {name:?}");
|
||||
// let call = parse_call(element);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let Some(next_element) = element.next_sibling_element() else {
|
||||
println!("exiting...");
|
||||
break;
|
||||
};
|
||||
element = next_element;
|
||||
}
|
||||
}
|
||||
|
||||
/// Return None if this is not an endpoint.
|
||||
///
|
||||
/// Will panic if it looks like an endpoint and has trouble parsing.
|
||||
fn parse_call(element: ElementRef) -> Option<Call> {
|
||||
let name = element.value().id().unwrap();
|
||||
|
||||
// <p><span class="endpoint"><code>POST /api/1/vehicles/{id}/command/auto_conditioning_start</code></span></p>
|
||||
// This section determines if this is an endpoint or not.
|
||||
let (fragment, element) = next(element);
|
||||
let url = fragment.select(&selector("code")).next()?.inner_html();
|
||||
if !url.starts_with("GET ") && !url.starts_with("POST ") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (method, url) = url.split_once(' ').unwrap();
|
||||
println!("{} {}", method, url);
|
||||
|
||||
// <p>scopes: <em>vehicle_cmds</em></p>
|
||||
let (fragment, element) = next(element);
|
||||
let scopes = fragment
|
||||
.select(&selector("em"))
|
||||
.map(|e| e.inner_html())
|
||||
.map(|e| Scope::from_str(&e))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// 4 <div class="highlight"> nodes containing example requests in different languages.
|
||||
// TODO: Skip for now
|
||||
let mut count = 0;
|
||||
let mut element = element;
|
||||
loop {
|
||||
let (fragment, new_element) = next(element);
|
||||
element = new_element;
|
||||
if fragment
|
||||
.select(&selector(r#"div[class="highlight"]"#))
|
||||
.next()
|
||||
.is_none()
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
count += 1;
|
||||
if count == 10 {
|
||||
panic!("Too many examples");
|
||||
}
|
||||
}
|
||||
if count == 0 && name != "api-status" {
|
||||
panic!("No examples for {}", name);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn next(element: ElementRef) -> (Html, ElementRef) {
|
||||
let element = element.next_sibling_element().unwrap();
|
||||
let html = Html::parse_fragment(&element.html());
|
||||
(html, element)
|
||||
}
|
||||
|
||||
fn selector(s: &str) -> Selector {
|
||||
Selector::parse(s).unwrap()
|
||||
}
|
93
tesla_api_coverage/src/main.rs
Normal file
93
tesla_api_coverage/src/main.rs
Normal file
|
@ -0,0 +1,93 @@
|
|||
mod fleet;
|
||||
mod vehicle_command;
|
||||
|
||||
use clap::Parser;
|
||||
use scraper::Element;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use tracing::info;
|
||||
|
||||
const TIMDORR_URL: &str =
|
||||
"https://raw.githubusercontent.com/timdorr/tesla-api/master/ownerapi_endpoints.json";
|
||||
const TIMDORR_FILE: &str = "timdorr.json";
|
||||
const VEHICLE_COMMAND_URL: &str = "https://raw.githubusercontent.com/teslamotors/vehicle-command/main/cmd/tesla-control/commands.go";
|
||||
const VEHICLE_COMMAND_FILE: &str = "vehicle_command.go";
|
||||
const FLEET_API_URL: &str = "https://developer.tesla.com/docs/fleet-api";
|
||||
const FLEET_API_FILE: &str = "fleet.html";
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(author, version)]
|
||||
struct Cli {
|
||||
/// Use the cached html if exists, to avoid making requests.
|
||||
#[clap(short, long)]
|
||||
cached: bool,
|
||||
|
||||
#[clap(short = 'v', long)]
|
||||
only_vehicle_command: bool,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
let args = Cli::parse();
|
||||
|
||||
// let timorr = cache_fetch(TIMDORR_URL, TIMDORR_FILE, args.cache).await;
|
||||
//
|
||||
// let fleet_html = cache_fetch(
|
||||
// FLEET_API_URL,
|
||||
// FLEET_API_FILE,
|
||||
// args.cache,
|
||||
// )
|
||||
// .await;
|
||||
//
|
||||
// let command_golang = cache_fetch(
|
||||
// VEHICLE_COMMAND_URL,
|
||||
// VEHICLE_COMMAND_FILE,
|
||||
// args.cache,
|
||||
// ).await;
|
||||
|
||||
let (timorr, fleet_html, command_golang) = tokio::join!(
|
||||
cache_fetch(TIMDORR_URL, TIMDORR_FILE, args.cached),
|
||||
cache_fetch(FLEET_API_URL, FLEET_API_FILE, args.cached),
|
||||
cache_fetch(VEHICLE_COMMAND_URL, VEHICLE_COMMAND_FILE, args.cached)
|
||||
);
|
||||
|
||||
let mut vehicle_command = true;
|
||||
let mut fleet_api = true;
|
||||
let mut timdorr = true;
|
||||
|
||||
if args.only_vehicle_command {
|
||||
fleet_api = false;
|
||||
timdorr = false;
|
||||
}
|
||||
|
||||
if fleet_api {
|
||||
fleet::parse(&fleet_html);
|
||||
}
|
||||
|
||||
if vehicle_command {
|
||||
vehicle_command::parse(&command_golang);
|
||||
}
|
||||
}
|
||||
|
||||
async fn cache_fetch(url: &str, filename: &str, cache: bool) -> String {
|
||||
// Write to where this project root is, not in the parent project.
|
||||
let mut path = PathBuf::new();
|
||||
path.push(env!("CARGO_MANIFEST_DIR"));
|
||||
path.push("cached");
|
||||
path.push(filename);
|
||||
|
||||
if cache && path.exists() {
|
||||
info!("Using cache: {path:?}");
|
||||
return std::fs::read_to_string(path).unwrap();
|
||||
}
|
||||
|
||||
info!("Fetching {url} -> {path:?}");
|
||||
let response = reqwest::get(url).await.unwrap();
|
||||
|
||||
let html = response.text().await.unwrap();
|
||||
|
||||
std::fs::write(path, &html).unwrap();
|
||||
|
||||
html
|
||||
}
|
209
tesla_api_coverage/src/vehicle_command.rs
Normal file
209
tesla_api_coverage/src/vehicle_command.rs
Normal file
|
@ -0,0 +1,209 @@
|
|||
use nom::branch::alt;
|
||||
use nom::bytes::complete::{tag, take_until, take_while};
|
||||
use nom::character::complete::{char, line_ending, space0, space1, tab};
|
||||
use nom::combinator::opt;
|
||||
use nom::multi::{many0, many1};
|
||||
use nom::IResult;
|
||||
use tracing::{trace, warn};
|
||||
|
||||
pub fn parse(s: &str) -> () {
|
||||
// Seek all the way to: var commands = map[string]*Command{\n
|
||||
// Afterwards has the first map entry.
|
||||
let commands_start = "var commands = map[string]*Command{\n";
|
||||
let offset = s.find(commands_start).unwrap();
|
||||
let s = &s[offset + commands_start.len()..];
|
||||
|
||||
let (go, entries) = many1(map_entry)(s).unwrap();
|
||||
|
||||
dbg!(&entries);
|
||||
|
||||
warn!("todo: parse")
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MapEntry {
|
||||
endpoint: String,
|
||||
help: String,
|
||||
// requires_auth: bool,
|
||||
// requires_fleet: bool,
|
||||
}
|
||||
|
||||
fn map_entry(s: &str) -> IResult<&str, MapEntry> {
|
||||
// "unlock": &Command{
|
||||
// help: "Unlock vehicle",
|
||||
// requiresAuth: true,
|
||||
// requiresFleetAPI: false,
|
||||
// args: []Argument{
|
||||
// Argument{name: "TEMP", help: "Desired temperature (e.g., 70f or 21c; defaults to Celsius)"},
|
||||
// Argument{name: "ROLE", help: "One of: owner, driver"},
|
||||
// },
|
||||
// handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
|
||||
// return car.Unlock(ctx)
|
||||
// },
|
||||
// },
|
||||
|
||||
short_trace("--- map entry ---", s);
|
||||
|
||||
// endpoint
|
||||
short_trace("endpoint", s);
|
||||
let (s, _) = ignore_whitespace(s)?;
|
||||
let (s, endpoint) = quoted_string(s)?;
|
||||
let (s, _) = until_eol(s)?;
|
||||
|
||||
// help
|
||||
short_trace("help", s);
|
||||
let (s, _) = ignore_whitespace(s)?;
|
||||
let (s, _) = tag("help:")(s)?;
|
||||
let (s, _) = ignore_whitespace(s)?;
|
||||
let (s, help) = quoted_string(s)?;
|
||||
let (s, _) = tag(",")(s)?;
|
||||
|
||||
// requiresAuth
|
||||
short_trace("requiresAuth", s);
|
||||
let (s, requires_auth) = bool_field_or_false(s, "requiresAuth:")?;
|
||||
|
||||
// requiresFleetAPI
|
||||
short_trace("requiresFleetAPI", s);
|
||||
let (s, requires_fleet) = bool_field_or_false(s, "requiresFleetAPI:")?;
|
||||
|
||||
// required args
|
||||
short_trace("required args", s);
|
||||
let (s, required_args) = args(s, "args: []Argument{")?;
|
||||
|
||||
// optional args
|
||||
short_trace("optional args", s);
|
||||
let (s, optional_args) = args(s, "optional: []Argument{")?;
|
||||
|
||||
// check and ignore the handler, as there's not really much data we can take out of it.
|
||||
let (s, _) = ignore_whitespace(s)?;
|
||||
|
||||
let (s, _) = take_until("},")(s)?;
|
||||
let (s, _) = tag("},")(s)?;
|
||||
|
||||
// And the end of the record...
|
||||
let (s, _) = take_until("},")(s)?;
|
||||
let (s, _) = tag("},")(s)?;
|
||||
|
||||
dbg!(endpoint, help, requires_auth, requires_fleet);
|
||||
|
||||
Ok((
|
||||
s,
|
||||
MapEntry {
|
||||
endpoint: endpoint.to_string(),
|
||||
help: help.to_string(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// Ignore the quotes and return the inner string.
|
||||
/// e.g. "unlock"
|
||||
fn quoted_string(s: &str) -> IResult<&str, &str> {
|
||||
short_trace("quoted string", s);
|
||||
let (s, _) = char('"')(s)?;
|
||||
let (s, string) = take_until("\"")(s)?;
|
||||
let (s, _) = char('"')(s)?;
|
||||
Ok((s, string))
|
||||
}
|
||||
|
||||
fn ignore_whitespace(s: &str) -> IResult<&str, ()> {
|
||||
short_trace("ignore whitespace", s);
|
||||
let (s, ws) = many0(alt((tag("\t"), space1, line_ending)))(s)?;
|
||||
short_trace("ignore whitespace afterwards", s);
|
||||
Ok((s, ()))
|
||||
}
|
||||
|
||||
fn until_eol(s: &str) -> IResult<&str, &str> {
|
||||
short_trace("eol", s);
|
||||
let (s, line) = take_until("\n")(s)?;
|
||||
let (s, _) = line_ending(s)?;
|
||||
short_trace("eol afterwards", s);
|
||||
Ok((s, line))
|
||||
}
|
||||
|
||||
fn str_to_bool(s: &str) -> IResult<&str, bool> {
|
||||
short_trace("bool", s);
|
||||
let (s, bool_str) = alt((tag("true"), tag("false")))(s)?;
|
||||
let bool = match bool_str {
|
||||
"true" => true,
|
||||
"false" => false,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
short_trace("bool afterwards", s);
|
||||
Ok((s, bool))
|
||||
}
|
||||
|
||||
/// If the field isn't there, assume false.
|
||||
fn bool_field<'a>(field_tag: &str) -> impl Fn(&'a str) -> IResult<&'a str, bool> + '_ {
|
||||
return move |s: &str| -> IResult<&'a str, bool> {
|
||||
let (s, _) = ignore_whitespace(s)?;
|
||||
let (s, _) = tag(field_tag)(s)?;
|
||||
let (s, _) = ignore_whitespace(s)?;
|
||||
let (s, value) = str_to_bool(s)?;
|
||||
let (s, _) = tag(",")(s)?;
|
||||
|
||||
Ok((s, value))
|
||||
};
|
||||
}
|
||||
|
||||
fn bool_field_or_false<'a>(s: &'a str, field_tag: &str) -> IResult<&'a str, bool> {
|
||||
let (s, value) = opt(bool_field(field_tag))(s)?;
|
||||
return Ok((s, value.unwrap_or(false)));
|
||||
}
|
||||
|
||||
struct Arg {
|
||||
name: String,
|
||||
help: String,
|
||||
}
|
||||
|
||||
fn args<'a>(s: &'a str, field_tag: &str) -> IResult<&'a str, Vec<Arg>> {
|
||||
short_trace("args", s);
|
||||
|
||||
let (s, _) = ignore_whitespace(s)?;
|
||||
let (s, maybe_field) = opt(tag(field_tag))(s)?;
|
||||
if maybe_field.is_none() {
|
||||
trace!("no arg record");
|
||||
return Ok((s, vec![]));
|
||||
}
|
||||
|
||||
let (s, _) = ignore_whitespace(s)?;
|
||||
let (s, args) = many1(arg)(s)?;
|
||||
let (s, _) = ignore_whitespace(s)?;
|
||||
let (s, _) = tag("},")(s)?;
|
||||
short_trace("args afterwards", s);
|
||||
Ok((s, args))
|
||||
}
|
||||
|
||||
fn arg(s: &str) -> IResult<&str, Arg> {
|
||||
short_trace("arg", s);
|
||||
let (s, _) = ignore_whitespace(s)?;
|
||||
let (s, _) = tag("Argument{")(s)?;
|
||||
let (s, _) = ignore_whitespace(s)?;
|
||||
let (s, _) = tag("name:")(s)?;
|
||||
let (s, _) = ignore_whitespace(s)?;
|
||||
let (s, name) = quoted_string(s)?;
|
||||
let (s, _) = ignore_whitespace(s)?;
|
||||
let (s, _) = tag(",")(s)?;
|
||||
let (s, _) = ignore_whitespace(s)?;
|
||||
let (s, _) = tag("help:")(s)?;
|
||||
let (s, _) = ignore_whitespace(s)?;
|
||||
let (s, help) = quoted_string(s)?;
|
||||
let (s, _) = opt(tag(","))(s)?;
|
||||
let (s, _) = ignore_whitespace(s)?;
|
||||
let (s, _) = tag("},")(s)?;
|
||||
short_trace("arg afterwards", s);
|
||||
Ok((
|
||||
s,
|
||||
Arg {
|
||||
name: name.to_string(),
|
||||
help: help.to_string(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn short_trace(prefix: &str, s: &str) {
|
||||
let mut max_len_left = 20;
|
||||
if s.len() < max_len_left {
|
||||
max_len_left = s.len();
|
||||
}
|
||||
trace!("{}: {:?}...", prefix, &s[0..max_len_left])
|
||||
}
|
Loading…
Reference in a new issue